# Dialog

Display a focused task in an overlay with a title and a clear close action.

Rename project form

[Complete TSX file](/examples/dialog/default.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import { useState } from "react";

export default function Example() {
  const [open, setOpen] = useState(false);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger render={<Button variant="outline" />}>
        Rename project
      </DialogTrigger>
      <DialogPopup>
        <Form
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Rename project</DialogTitle>
            <DialogDescription>
              Everyone in the workspace will see the new name.
            </DialogDescription>
          </DialogHeader>
          <DialogPanel>
            <Field name="name">
              <FieldLabel>Project name</FieldLabel>
              <Input defaultValue="Website Redesign" required />
              <FieldError match="valueMissing">
                Enter a project name.
              </FieldError>
            </Field>
          </DialogPanel>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Cancel
            </DialogClose>
            <Button type="submit">Save</Button>
          </DialogFooter>
        </Form>
      </DialogPopup>
    </Dialog>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/dialog
```

With npm: `npx shadcn@4.18.0 add @siteplane/dialog`. [Registry JSON](/registry/dialog.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/data-list @siteplane/form @siteplane/input @siteplane/label @siteplane/menu
```

Field is installed with Input.

## Usage

```tsx
import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogTrigger,
  DialogPopup,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/siteplane/components/dialog";

<Dialog>
  <DialogTrigger render={<Button />}>Open dialog</DialogTrigger>
  <DialogPopup>
    <DialogHeader>
      <DialogTitle>Project details</DialogTitle>
      <DialogDescription>Review the current project.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose render={<Button />}>Done</DialogClose>
    </DialogFooter>
  </DialogPopup>
</Dialog>;
```

## Composition

```text
Dialog
├── DialogTrigger
└── DialogPopup
    ├── DialogHeader
    │   ├── DialogTitle
    │   └── DialogDescription
    ├── DialogPanel
    └── DialogFooter
        └── DialogClose
```

`DialogPopup` includes the portal, backdrop, viewport, popup, and close icon. Do not add a second portal or backdrop. `DialogContent` and `DialogOverlay` are the shadcn names of `DialogPopup` and `DialogBackdrop`; both keep working.

## Size

`DialogPopup` takes `size`, the width on the kit's modal scale: `xs` 400 px, `sm` 440 px (the default), `md` 600 px or `lg` 880 px. Use `md` or `lg` for a form with two columns or a wide table. The size applies from 640 px up; below that, every dialog is a full-width sheet at the bottom edge. A `max-w-*` class in `className` still sets its own width, also below 640 px. [Alert Dialog](/docs/components/alert-dialog/) takes the same sizes.

## Open from a menu

Pass `open` and `onOpenChange` when something other than `DialogTrigger` opens the dialog, such as a menu item. Render the dialog outside the menu so it stays mounted after the menu closes.

Share dialog opened from a menu

[Complete TSX file](/examples/dialog/controlled.tsx)

```tsx
"use client";

import { CopyIcon, ShareIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
} from "@/siteplane/components/dialog";
import { Input } from "@/siteplane/components/input";
import { Label } from "@/siteplane/components/label";
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/siteplane/components/menu";
import { useId, useState } from "react";

export default function Example() {
  const linkId = useId();
  const [open, setOpen] = useState(false);
  return (
    <>
      <Menu>
        <MenuTrigger render={<Button variant="outline" />}>
          Project actions
        </MenuTrigger>
        <MenuPopup align="start">
          <MenuItem>
            <CopyIcon />
            Duplicate
          </MenuItem>
          <MenuItem onClick={() => setOpen(true)}>
            <ShareIcon />
            Share…
          </MenuItem>
        </MenuPopup>
      </Menu>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>Share project</DialogTitle>
            <DialogDescription>
              Anyone in the workspace with this link can view the project.
            </DialogDescription>
          </DialogHeader>
          <DialogPanel className="grid gap-3">
            <Label htmlFor={linkId}>Project link</Label>
            <Input
              id={linkId}
              defaultValue="https://app.example.com/projects/website-redesign"
              readOnly
            />
          </DialogPanel>
          <DialogFooter>
            <DialogClose render={<Button />}>Done</DialogClose>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    </>
  );
}
```

## Nested dialogs

Open a dialog from inside another dialog. The parent stays visible behind it, scaled down and faded by 10 % for each dialog on top, so the context stays in view. Each root owns its state; Escape closes the topmost dialog, and closing it returns focus to its trigger in the parent.

Nested team member dialogs

[Complete TSX file](/examples/dialog/nested.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  DataList,
  DataListItem,
  DataListLabel,
  DataListValue,
} from "@/siteplane/components/data-list";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import { useState } from "react";

export default function Example() {
  const [editOpen, setEditOpen] = useState(false);
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Manage member
      </DialogTrigger>
      <DialogPopup>
        <DialogHeader>
          <DialogTitle>Manage team member</DialogTitle>
          <DialogDescription>
            View and update this member of your workspace.
          </DialogDescription>
        </DialogHeader>
        <DialogPanel>
          <DataList orientation="vertical">
            <DataListItem>
              <DataListLabel>Name</DataListLabel>
              <DataListValue>Ben Carter</DataListValue>
            </DataListItem>
            <DataListItem>
              <DataListLabel>Email</DataListLabel>
              <DataListValue>ben.carter@example.com</DataListValue>
            </DataListItem>
          </DataList>
        </DialogPanel>
        <DialogFooter>
          <DialogClose render={<Button variant="ghost" />}>Close</DialogClose>
          <Dialog open={editOpen} onOpenChange={setEditOpen}>
            <DialogTrigger render={<Button />}>Edit details</DialogTrigger>
            <DialogPopup>
              <Form
                onSubmit={(event) => {
                  event.preventDefault();
                  setEditOpen(false);
                }}
              >
                <DialogHeader>
                  <DialogTitle>Edit details</DialogTitle>
                  <DialogDescription>
                    Update the member&apos;s name and email address.
                  </DialogDescription>
                </DialogHeader>
                <DialogPanel className="grid gap-4">
                  <Field name="name">
                    <FieldLabel>Name</FieldLabel>
                    <Input defaultValue="Ben Carter" required />
                    <FieldError match="valueMissing">Enter a name.</FieldError>
                  </Field>
                  <Field name="email">
                    <FieldLabel>Email</FieldLabel>
                    <Input
                      type="email"
                      defaultValue="ben.carter@example.com"
                      required
                    />
                    <FieldError>Enter a valid email address.</FieldError>
                  </Field>
                </DialogPanel>
                <DialogFooter>
                  <DialogClose render={<Button variant="ghost" />}>
                    Cancel
                  </DialogClose>
                  <Button type="submit">Save changes</Button>
                </DialogFooter>
              </Form>
            </DialogPopup>
          </Dialog>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  );
}
```

## Scrollable content

Put long content in `DialogPanel`. When the dialog reaches the available height, the panel scrolls while the header and footer stay in view. The dialog opens at the top of its content: when its first focusable element, such as a confirmation field under long terms, starts below the visible part of the panel, the popup itself takes focus, and Tab moves on to that element.

Terms of service with scrollable content

[Complete TSX file](/examples/dialog/scrolling.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";

const sections = [
  {
    title: "Your account",
    text: "You are responsible for activity in your account and for keeping your sign-in details secure. Tell us right away if you notice access you don't recognize.",
  },
  {
    title: "Workspace content",
    text: "You keep ownership of the projects, files and comments you add. We store and process them only to provide the service to your workspace.",
  },
  {
    title: "Acceptable use",
    text: "Don't upload malicious code, try to access other workspaces, or use the service to send unsolicited messages.",
  },
  {
    title: "Plans and billing",
    text: "Paid plans renew automatically at the end of each billing period. You can cancel at any time, and access continues until the period ends.",
  },
  {
    title: "Deleted projects",
    text: "Deleted projects stay in the trash for 30 days. After that, they are removed permanently and can't be restored.",
  },
  {
    title: "Service availability",
    text: "We may pause parts of the service for maintenance. Planned maintenance is announced to workspace owners in advance.",
  },
  {
    title: "Changes to these terms",
    text: "We notify workspace owners at least 30 days before a material change takes effect. Continuing to use the service means you accept the updated terms.",
  },
  {
    title: "Closing your account",
    text: "You can export your data and close your account from the account settings. Workspace owners can also remove members at any time.",
  },
  {
    title: "Contact",
    text: "Send questions about these terms to your workspace administrator or contact support from the help menu.",
  },
];

export default function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Review terms
      </DialogTrigger>
      <DialogPopup showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>Updated terms of service</DialogTitle>
          <DialogDescription>
            Review the changes before you continue.
          </DialogDescription>
        </DialogHeader>
        <DialogPanel className="grid gap-4">
          {sections.map((section) => (
            <div key={section.title} className="grid gap-1 text-sm">
              <h3 className="font-medium">{section.title}</h3>
              <p className="text-muted-foreground">{section.text}</p>
            </div>
          ))}
        </DialogPanel>
        <DialogFooter>
          <DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
          <DialogClose render={<Button />}>Accept</DialogClose>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  );
}
```

## API reference

### Dialog

The root renders no DOM element. These properties retain the installed Base UI contract.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| open | boolean | uncontrolled | Controlled open state. |
| defaultOpen | boolean | false | Initial uncontrolled open state. |
| onOpenChange | (open, eventDetails) => void | unset | Open/close request. Details include reason, event, cancel(), and preventUnmountOnClose(). |
| onOpenChangeComplete | (open: boolean) => void | unset | Runs after opening or closing animations complete. |
| modal | boolean \| "trap-focus" | true | true traps focus, locks page scrolling and blocks outside interaction. false permits outside interaction. trap-focus traps focus without the other modal restrictions. |
| disablePointerDismissal | boolean | false | Prevents outside-press dismissal; also prevents focus-out dismissal in non-modal mode. Escape still closes the dialog. |
| actionsRef | RefObject<DialogPrimitive.Root.Actions \| null> | unset | Imperative close() and unmount(); manual unmount is for externally managed exit animations. |
| handle | Dialog handle | unset | Connects detached triggers and imperative handle methods. |
| triggerId | string \| null | unset | Identifies the active trigger when controlling open state with multiple triggers. |
| defaultTriggerId | string \| null | unset | Initial trigger association, for example with defaultOpen. |
| children | ReactNode \| ({ payload }) => ReactNode | unset | Parts, or a render function receiving the active trigger payload. |
### DialogPopup / DialogContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| size | "xs" \| "sm" \| "md" \| "lg" | sm | Popup width from 640 px up: 400, 440, 600 or 880 px. |
| showCloseButton | boolean | true | Renders the built-in close icon action. When false, provide another accessible close action. |
| closeProps | DialogPrimitive.Close.Props | unset | Overrides the built-in close action, including aria-label (default: Close). |
| portalProps | DialogPrimitive.Portal.Props | unset | Configures the included portal; container defaults to document.body, keepMounted to false. |
| initialFocus | boolean \| RefObject \| (interactionType) => target | first tabbable element | Focus target when opening. By default the first tabbable element, or the popup itself on touch and when that element starts out of view. |
| finalFocus | boolean \| RefObject \| (interactionType) => target | foundation default | Focus target when closing. |
| className | string | unset | Adds popup styling. |
| render | ReactElement \| render function | div | Composes the popup onto another element. |
| children | ReactNode | unset | Popup content before the optional built-in close button. |
The styled parts take a string `className`, which they merge with their own
classes through `cn`. Style a state through the part's data attributes, such as
`data-open`, rather than a `className` function.

The remaining popup props follow [Base UI
Popup](https://base-ui.com/react/components/dialog#popup). Keep the portal in
the default location unless there is a concrete container requirement. The
supplied composition carries the Siteplane UI provider settings into its portal.

### Content layout parts

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| DialogHeader | div props + render | div | Groups title/description with header spacing. className adds local styling. |
| DialogFooter | div props + render | div | Lays out actions in a row, aligned to the end. |
| DialogPanel.scrollFade | boolean | true | Enables the scroll area edge fade. |
| DialogPanel | div props + render | div inside ScrollArea | Body wrapped in a ScrollArea; className styles the inner panel. When the content exceeds the available height, it scrolls between the header and footer. |
| DialogTitle | DialogPrimitive.Title.Props | h2 | Accessible dialog title with heading typography: 18 px in the title weight, --sp-rw-font-weight-title (450), which Theming lists. |
| DialogDescription | DialogPrimitive.Description.Props | p | Accessible description with secondary text styling. |
All layout parts accept `children`, `className`, native props and `render` through their corresponding contract. Use `React.ComponentProps<typeof DialogPanel>` (or another part) to derive its TypeScript props; Siteplane UI does not export separate named prop aliases for these wrappers.

### Trigger, close and lower-level exports

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| DialogTrigger | DialogPrimitive.Trigger.Props | button | Opens its root. Supports render, disabled, id, handle and payload. |
| DialogClose | DialogPrimitive.Close.Props | button | Requests closing. Supports render and disabled. |
| DialogPortal | DialogPrimitive.Portal.Props | body / keepMounted=false | Moves content to a container. Already included in DialogPopup. |
| DialogBackdrop / DialogOverlay | DialogPrimitive.Backdrop.Props | div | Styled overlay; already included in DialogPopup. |
| DialogViewport | DialogPrimitive.Viewport.Props | div | Styled overlay layout container; already included in DialogPopup. |
| DialogPrimitive | Base UI Dialog namespace | — | Public foundation namespace for advanced composition and its types. It is not the styled Siteplane UI popup. |
| DialogCreateHandle<Payload>() | () => Dialog handle | — | Creates a handle; retain the instance with state or another stable owner. |
Share a stable `DialogCreateHandle` between the root and detached triggers; the
root's render function receives the active trigger's `payload`. The handle
exposes `open(triggerId | null)`, `openWithPayload(payload)`, `close()`, and
`isOpen`. Call methods in an event handler or effect after the associated root
has mounted. Controlled roots still need their state updated through
`onOpenChange`.

Use the ready-made `DialogPopup` for normal usage. A custom low-level composition must supply its own popup, close action, provider scope and positioning; do not combine a second low-level portal/backdrop with `DialogPopup`. In particular, setting `modal={false}` does not remove the backdrop or full-screen viewport included by the styled composition. For an anchored, non-blocking interaction use the appropriate Popover component.

Full inherited part contracts: [Root](https://base-ui.com/react/components/dialog#root), [Trigger](https://base-ui.com/react/components/dialog#trigger), [Portal](https://base-ui.com/react/components/dialog#portal), [Backdrop](https://base-ui.com/react/components/dialog#backdrop), [Viewport](https://base-ui.com/react/components/dialog#viewport), [Title](https://base-ui.com/react/components/dialog#title), [Description](https://base-ui.com/react/components/dialog#description), and [Close](https://base-ui.com/react/components/dialog#close). The [installed source](/source/dialog) is authoritative for the version in your project.

## Usage notes

Provide `DialogTitle` on every dialog, and a description when it adds useful context. Label every form control. Tab and Shift+Tab stay within a modal dialog; Escape requests closing. Always retain an accessible close action, including when hiding the default icon. Verify focus restoration when the original trigger is removed from the page.

Opening focuses the first tabbable element. The popup itself takes focus instead on touch devices, so the on-screen keyboard doesn't open, and when that element starts out of view in a scrolling panel, so a long dialog opens at its top and a screen reader reads its title first. Closing returns focus to the trigger or the previously focused element; use `initialFocus` and `finalFocus` to choose other targets.

Submit forms with `Button type="submit"` and close the dialog only after the submission succeeds; a submit button wrapped in `DialogClose` would close immediately. Keep the dialog open while an asynchronous request is pending or has failed. To confirm before discarding unsaved changes, see [Close confirmation](/docs/components/alert-dialog/#close-confirmation).

When the user must make a decision before going on, use [Alert Dialog](/docs/components/alert-dialog/) instead. Keep actions and scrollable content reachable at mobile widths and with zoom. The kit carries shared size, theme, and motion settings through its normal portal; no additional provider is required by these examples.
