# Dialog (/docs/dialog)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/dialog" />

## Basic [#basic]

<Preview cssProperties="dialogPlaygroundCssProperties">
  <DialogExample />

  <Preview.Code>
    {`
          import {
            Dialog,
            DialogTrigger,
            Button,
            DialogContent,
            DialogHeader,
            DialogTitle,
            DialogCloseIcon,
            DialogDescription,
            DialogFooter,
            DialogClose,
          } from "moduix";

          export function DialogDemo() {
            return (
              <Dialog>
                <DialogTrigger render={<Button />}>View notifications</DialogTrigger>
                <DialogContent>
                  <DialogHeader>
                    <DialogTitle>Notifications</DialogTitle>
                    <DialogCloseIcon />
                    <DialogDescription>You are all caught up. Good job!</DialogDescription>
                  </DialogHeader>
                  <DialogFooter>
                    <DialogClose render={<Button variant="outline" />}>Close</DialogClose>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSSProperties>
    {(context) => <DialogCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <DialogCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#anatomy]

`Dialog` combines a root state machine with one visible popup surface and internal service layers.
Use `DialogContent` as the popup container; it automatically renders `portal`, `backdrop`, and
`viewport` slots for overlay behavior.

```text
Dialog
├─ DialogTrigger
└─ DialogContent
   ├─ portal (service slot)
   │  ├─ backdrop (service slot)
   │  └─ viewport (service slot)
   │     └─ popup surface
   │        ├─ DialogCloseIcon (optional)
   │        ├─ DialogHeader
   │        │  ├─ DialogTitle
   │        │  └─ DialogDescription (optional)
   │        ├─ DialogBody (optional)
   │        └─ DialogFooter (optional)
   │           └─ DialogClose
```

```tsx
<Dialog>
  <DialogTrigger render={<Button />}>Edit profile</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogCloseIcon />
      <DialogDescription>Update your public information.</DialogDescription>
    </DialogHeader>
    <DialogBody>
      <p>Profile fields and form controls go here.</p>
    </DialogBody>
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
      <DialogClose render={<Button />}>Save</DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

| Part                | Role                                                                                               |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `Dialog`            | Root state and interaction model. Use `open`, `defaultOpen`, `onOpenChange`, and `modal` here.     |
| `DialogTrigger`     | Opens the dialog from user interaction. Use `handle` when the trigger lives outside the tree.      |
| `DialogContent`     | Popup container. It renders the internal `portal`, `backdrop`, and `viewport` slots automatically. |
| `DialogHeader`      | Groups title-level content at the top of the popup.                                                |
| `DialogTitle`       | Accessible dialog title announced by assistive technology.                                         |
| `DialogDescription` | Optional supporting text announced with the title.                                                 |
| `DialogBody`        | Optional content region for forms, text, or custom layout.                                         |
| `DialogFooter`      | Optional actions row for submit/cancel buttons.                                                    |
| `DialogCloseIcon`   | Optional icon-only close button. Pass children to replace the default icon.                        |
| `DialogClose`       | Closes the dialog from action buttons while preserving built-in behavior.                          |

In most cases, keep default overlay behavior and style only visible popup parts with `className`
(`DialogContent`, `DialogHeader`, `DialogBody`, `DialogFooter`). Customize service slots with
`classNames` (`portal`, `backdrop`, `viewport`) only when you need special layering, positioning,
or overlay visuals.

## Composition [#composition]

Use `Dialog` for root state and behavior props such as `open`, `defaultOpen`, `onOpenChange`, and
`modal`. Base UI root behavior is passed through: `onOpenChangeComplete`,
`disablePointerDismissal`, `actionsRef`, `handle`, `triggerId`, and `defaultTriggerId` are available
on `Dialog`.

`className` styles the visible popup. `classNames` styles the internal service slots that are
hidden from the default composition. Use `container`, `slotProps`, and `withBackdrop={false}` when
you need the matching Base UI escape hatches. Common cases are `slotProps.portal.keepMounted`,
`slotProps.backdrop.forceRender`, and viewport attributes or refs through `slotProps.viewport`.
`DialogContent` itself accepts popup props such as `initialFocus` and `finalFocus`.

```tsx
<DialogContent
  className={styles.popup}
  classNames={{
    portal: styles.portal,
    backdrop: styles.backdrop,
    viewport: styles.viewport,
  }}
  slotProps={{
    portal: { keepMounted: true },
    backdrop: { forceRender: true },
  }}
/>
```

`DialogTrigger`, `DialogClose`, `DialogTitle`, `DialogDescription`, and `DialogContent` also accept
their matching Base UI primitive props, including `render` where Base UI supports element
replacement.

## Examples [#examples]

### Controlled [#controlled]

Control the open state from React when the dialog needs to coordinate with other UI.

<Preview>
  <ControlledDialogExample />

  <Preview.Code>
    {`
          import {
            Button,
            Dialog,
            DialogContent,
            DialogHeader,
            DialogTitle,
            DialogDescription,
            DialogFooter,
            DialogClose,
          } from "moduix";
          import { useState, Fragment } from "react";

          export function ControlledDialogDemo() {
            const [open, setOpen] = useState(false);

            return (
              <Fragment>
                <Button type="button" onClick={() => setOpen(true)}>
                  Open controlled dialog
                </Button>
                <Dialog open={open} onOpenChange={setOpen}>
                  <DialogContent>
                    <DialogHeader>
                      <DialogTitle>Publish changes?</DialogTitle>
                      <DialogDescription>
                        This will make the latest version visible to all users.
                      </DialogDescription>
                    </DialogHeader>
                    <DialogFooter>
                      <DialogClose render={<Button variant="outline" />}>Back to editing</DialogClose>
                      <DialogClose render={<Button />}>Publish</DialogClose>
                    </DialogFooter>
                  </DialogContent>
                </Dialog>
              </Fragment>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Handle [#handle]

Use `createDialogHandle` when the trigger lives outside the dialog tree or the dialog opens
programmatically.

<Preview>
  <DialogHandleExample />

  <Preview.Code>
    {`
          import {
            createDialogHandle,
            DialogTrigger,
            Button,
            Dialog,
            DialogContent,
            DialogHeader,
            DialogTitle,
            DialogDescription,
            DialogFooter,
            DialogClose,
          } from "moduix";
          import { useMemo, Fragment } from "react";

          export function DialogHandleDemo() {
            const dialogHandle = useMemo(() => createDialogHandle(), []);

            return (
              <Fragment>
                <DialogTrigger handle={dialogHandle} render={<Button variant="outline" />}>
                  Open from detached trigger
                </DialogTrigger>
                <Button type="button" onClick={() => dialogHandle.open(null)}>
                  Open programmatically
                </Button>

                <Dialog handle={dialogHandle}>
                  <DialogContent>
                    <DialogHeader>
                      <DialogTitle>Detached trigger</DialogTitle>
                      <DialogDescription>
                        This dialog is connected via createDialogHandle().
                      </DialogDescription>
                    </DialogHeader>
                    <DialogFooter>
                      <DialogClose render={<Button variant="outline" />}>Close</DialogClose>
                    </DialogFooter>
                  </DialogContent>
                </Dialog>
              </Fragment>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Scrollable Content [#scrollable-content]

Place `ScrollArea` inside `DialogBody` when the header and footer should remain visible while only
the body content scrolls.

<Preview>
  <ScrollableDialogExample />

  <Preview.Code>
    {`
          import {
            Dialog,
            DialogTrigger,
            Button,
            DialogContent,
            DialogHeader,
            DialogTitle,
            DialogCloseIcon,
            DialogDescription,
            DialogBody,
            DialogFooter,
            DialogClose,
            ScrollArea,
          } from "moduix";

          export function ScrollableDialogDemo() {
            return (
              <Dialog>
                <DialogTrigger render={<Button />}>Open long content</DialogTrigger>
                <DialogContent className={styles.scrollPopup}>
                  <DialogHeader>
                    <DialogTitle>Release checklist</DialogTitle>
                    <DialogCloseIcon />
                    <DialogDescription>
                      Review all items before publishing to production.
                    </DialogDescription>
                  </DialogHeader>
                  <DialogBody className={styles.scrollBody}>
                    <ScrollArea
                      className={styles.scrollArea}
                      classNames={{ content: styles.scrollContent }}
                    >
                      {sections.map((item) => (
                        <section key={item.title}>
                          <h3>{item.title}</h3>
                          <p>{item.body}</p>
                        </section>
                      ))}
                    </ScrollArea>
                  </DialogBody>
                  <DialogFooter>
                    <DialogClose render={<Button variant="outline" />}>Close</DialogClose>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .scrollPopup {
            display: flex;
            flex-direction: column;
            height: min(42rem, calc(100dvh - var(--spacing-10)));
            overflow: hidden;
          }

          .scrollBody {
            min-height: 0;
            flex: 1;
            overflow: hidden;
          }

          .scrollArea {
            height: 100%;
            min-height: 0;
          }

          .scrollContent {
            display: flex;
            flex-direction: column;
            gap: var(--spacing-5);
          }
        `}
  </Preview.CSS>

  <Preview.Data>
    {`
          const sections = [
            {
              title: "Database migrations",
              body: "Confirm migration scripts are idempotent and have rollback steps.",
            },
            {
              title: "Monitoring",
              body: "Check that the dashboard includes new API endpoints.",
            },
            {
              title: "Analytics",
              body: "Verify onboarding funnel events are firing.",
            },
            {
              title: "Staging",
              body: "Run smoke tests with a production-like dataset.",
            },
          ];
        `}
  </Preview.Data>
</Preview>

### Nested [#nested]

Dialogs can be nested. The parent popup receives Base UI nested-dialog attributes and CSS variables,
so it can visually recede while the child dialog is active.

<Preview>
  <NestedDialogExample />

  <Preview.Code>
    {`
          import {
            Dialog,
            DialogTrigger,
            Button,
            DialogContent,
            DialogHeader,
            DialogTitle,
            DialogDescription,
            DialogFooter,
            DialogClose,
          } from "moduix";

          export function NestedDialogDemo() {
            return (
              <Dialog>
                <DialogTrigger render={<Button />}>View notifications</DialogTrigger>
                <DialogContent>
                  <DialogHeader>
                    <DialogTitle>Notifications</DialogTitle>
                    <DialogDescription>You are all caught up. Good job!</DialogDescription>
                  </DialogHeader>
                  <DialogFooter>
                    <Dialog>
                      <DialogTrigger render={<Button variant="outline" />}>Customize</DialogTrigger>
                      <DialogContent>
                        <DialogHeader>
                          <DialogTitle>Customize notifications</DialogTitle>
                          <DialogDescription>Review your settings here.</DialogDescription>
                        </DialogHeader>
                        <DialogFooter>
                          <DialogClose render={<Button variant="outline" />}>Close</DialogClose>
                        </DialogFooter>
                      </DialogContent>
                    </Dialog>
                    <DialogClose render={<Button variant="outline" />}>Close</DialogClose>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Non-Modal [#non-modal]

Set `modal={false}` when the dialog should not trap focus, lock page scroll, or block pointer
interaction with the rest of the page. Pair it with `withBackdrop={false}` when no overlay should be
rendered.

<Preview>
  <NonModalDialogExample />

  <Preview.Code>
    {`
          import {
            Dialog,
            DialogTrigger,
            Button,
            DialogContent,
            DialogHeader,
            DialogTitle,
            DialogDescription,
            DialogFooter,
            DialogClose,
          } from "moduix";

          export function NonModalDialogDemo() {
            return (
              <Dialog modal={false}>
                <DialogTrigger render={<Button />}>Open non-modal dialog</DialogTrigger>
                <DialogContent withBackdrop={false}>
                  <DialogHeader>
                    <DialogTitle>Non-modal dialog</DialogTitle>
                    <DialogDescription>
                      The page remains interactive because modal behavior and backdrop are disabled.
                    </DialogDescription>
                  </DialogHeader>
                  <DialogFooter>
                    <DialogClose render={<Button variant="outline" />}>Close</DialogClose>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Custom Styles [#custom-styles]

For an outside close pattern, place `DialogCloseIcon` directly in `DialogContent` and position it
against the viewport corner. This keeps the close control independent from popup size and loading
content behavior.

<Preview>
  <CustomStylesDialogExample />

  <Preview.Code>
    {`
          import {
            Dialog,
            DialogTrigger,
            Button,
            DialogContent,
            DialogCloseIcon,
            DialogHeader,
            DialogTitle,
            DialogDescription,
            DialogBody,
            DialogFooter,
            DialogClose,
            CloseLineIcon,
          } from "moduix";

          export function CustomStylesDialogDemo() {
            return (
              <Dialog>
                <DialogTrigger render={<Button />}>Open outside close icon</DialogTrigger>
                <DialogContent
                  className={styles.customPopup}
                  classNames={{
                    portal: styles.customPortal,
                    backdrop: styles.customBackdrop,
                    viewport: styles.customViewport,
                  }}
                  slotProps={{
                    portal: { keepMounted: true },
                    backdrop: { forceRender: true },
                  }}
                >
                  <DialogCloseIcon
                    aria-label="Close profile dialog"
                    className={styles.customCloseIcon}
                  >
                    <CloseLineIcon />
                  </DialogCloseIcon>
                  <DialogHeader>
                    <DialogTitle>Edit profile</DialogTitle>
                    <DialogDescription>
                      This popup, backdrop, viewport, and close icon are styled with className and
                      classNames.
                    </DialogDescription>
                  </DialogHeader>
                  <DialogBody>
                    <p>Update the public profile fields and save changes.</p>
                  </DialogBody>
                  <DialogFooter>
                    <DialogClose render={<Button />}>Save</DialogClose>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .customPortal {
            pointer-events: auto;
          }

          .customBackdrop {
            background-color: rgb(15 23 42 / 0.56);
          }

          .customViewport {
            align-items: start;
            padding-top: var(--spacing-10);
          }

          .customPopup {
            position: relative;
            overflow: visible;
            --dialog-width: 26rem;
            --dialog-padding: var(--spacing-5);
          }

          .customCloseIcon {
            position: fixed;
            top: var(--spacing-4);
            right: var(--spacing-4);
            border: var(--border-width-sm) solid var(--dialog-border-color, var(--color-border));
            background-color: var(--dialog-bg, var(--color-popover));
            --dialog-close-icon-color: var(--color-muted-foreground);
          }
        `}
  </Preview.CSS>
</Preview>
