# Dialog

A window above the page for a focused task, such as editing a record. The page waits until it closes.

- **Import:** `import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "@booleanpress/ui/dialog"`
- **Radix Dialog:** <https://www.radix-ui.com/primitives/docs/components/dialog>
- **APG Dialog (Modal):** <https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/>
- **Page:** <https://ui.booleanpress.com/components/dialog> · @booleanpress/ui 0.1.0

## Usage

A dialog has a trigger, a title, and its content. `DialogTitle` names the dialog and `DialogDescription` describes it; both are read when it opens. `DialogBody` scrolls when the content is taller than the window, while the header and the footer stay in place.

```tsx
import { Button } from "@booleanpress/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@booleanpress/ui/dialog"

export function EditMailer() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Edit mailer</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Edit mailer</DialogTitle>
          <DialogDescription>Change the name and the sender address.</DialogDescription>
        </DialogHeader>
        <DialogBody>…</DialogBody>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button>Save</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

`size` sets the width: `sm` (384 px), `md` (512 px, the default) or `lg` (672 px). To open it from code, control it with `open` and `onOpenChange` and leave out the trigger; focus then returns to the element that had it when the dialog opened.

## Examples

### Basic

A form in a dialog, with Cancel and Save.

```tsx
import { Button } from "@booleanpress/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@booleanpress/ui/dialog"
import { Input } from "@booleanpress/ui/input"
import { Label } from "@booleanpress/ui/label"

export default function DialogBasic() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Edit mailer</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Edit mailer</DialogTitle>
          <DialogDescription>Change the name and the sender address.</DialogDescription>
        </DialogHeader>
        <DialogBody className="flex flex-col gap-4">
          <div className="flex flex-col gap-2">
            <Label htmlFor="mailer-name">Name</Label>
            <Input id="mailer-name" defaultValue="Primary mailer" />
          </div>
          <div className="flex flex-col gap-2">
            <Label htmlFor="mailer-from">Sender address</Label>
            <Input id="mailer-from" type="email" defaultValue="hello@example.com" />
          </div>
        </DialogBody>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <DialogClose asChild>
            <Button>Save</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

### Sizes

The three widths.

```tsx
import { Button } from "@booleanpress/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@booleanpress/ui/dialog"

const SIZES = [
  { size: "sm", label: "Small", width: "384 px" },
  { size: "md", label: "Medium", width: "512 px" },
  { size: "lg", label: "Large", width: "672 px" },
] as const

export default function DialogSizes() {
  return (
    <div className="flex flex-wrap gap-3">
      {SIZES.map(({ size, label, width }) => (
        <Dialog key={size}>
          <DialogTrigger asChild>
            <Button variant="outline">{label}</Button>
          </DialogTrigger>
          <DialogContent size={size}>
            <DialogHeader>
              <DialogTitle>{label} dialog</DialogTitle>
              <DialogDescription>At most {width} wide, and never wider than the window.</DialogDescription>
            </DialogHeader>
          </DialogContent>
        </Dialog>
      ))}
    </div>
  )
}
```

### Long content

Content taller than the window scrolls inside `DialogBody`; the title and the actions stay visible.

```tsx
import { Button } from "@booleanpress/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@booleanpress/ui/dialog"

const PARAGRAPHS = Array.from(
  { length: 24 },
  (_, i) => `${i + 1}. Imported contacts keep their subscription status. Contacts who unsubscribed stay unsubscribed, and no email is sent to them.`,
)

export default function DialogLongContent() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Review the import terms</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Import terms</DialogTitle>
          <DialogDescription>Read them before you continue.</DialogDescription>
        </DialogHeader>
        <DialogBody className="flex flex-col gap-3 text-sm">
          {PARAGRAPHS.map((text) => (
            <p key={text}>{text}</p>
          ))}
        </DialogBody>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <DialogClose asChild>
            <Button>Accept</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

### Opened from code

No trigger: `open` and `onOpenChange` control it, and focus returns to the button that opened it.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@booleanpress/ui/dialog"

export default function DialogControlled() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Check the connection
      </Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent size="sm">
          <DialogHeader>
            <DialogTitle>Connection works</DialogTitle>
            <DialogDescription>The mail server accepted the test message.</DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button onClick={() => setOpen(false)}>Done</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  )
}
```

## Accessibility

**Semantics.** A `div` with `role="dialog"` and `aria-modal="true"`, named by `DialogTitle` and described by `DialogDescription`. The rest of the page is hidden from assistive technology while it is open.

**Labels.** Every dialog needs a `DialogTitle`. If the design has no visible title, keep one for screen readers with `className="sr-only"`. The × button's name comes from the provider's `close` string.

**Focus.** Focus moves into the dialog when it opens, Tab and Shift+Tab stay inside, and focus returns to the trigger when it closes. Without a trigger, it returns to the element that had focus when the dialog opened, or to `returnFocusTo` when that element is gone.

**Known limits.**

- One dialog at a time. A dialog can open a select or a popover, not a second dialog.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Escape | Closes the dialog. |
| Tab | Moves to the next focusable element inside the dialog, wrapping from the last to the first. |
| Shift + Tab | Moves to the previous focusable element inside the dialog, wrapping from the first to the last. |

## API

### Dialog

Renders Radix Dialog.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  | Whether it starts open, when it controls itself. |
| `modal` | `boolean` |  | Keep it modal: the rest of the page cannot be reached while it is open. Leave it on. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether it is open, when you control it. Pair it with `onOpenChange`. |

### DialogBody

Renders a `div` and passes it every other prop.

### DialogClose

Renders Radix Dialog.Close and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### DialogContent

Renders Radix Dialog.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `deferPointerDownOutside` | `boolean` |  | When `true`, a `'pointerdown'` event outside of the layered element will wait for the interaction's click event before dispatching, allowing third-party code to stop propagation of later events and cancel dismissal. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
| `onOpenAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on open. Can be prevented. |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
| `returnFocusTo` | `ReturnFocusTarget` |  | Where focus goes on close when the element that opened the overlay no longer exists — for example after the confirmed action deleted the row it sat in. |
| `showCloseButton` | `boolean` | `true` | Render the × button. |
| `size` | `"sm" \| "lg" \| "md"` | `md` | Maximum width: 384, 512 or 672 px. |

### DialogDescription

Renders Radix Dialog.Description and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### DialogFooter

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showCloseButton` | `boolean` | `false` |  |

### DialogHeader

Renders a `div` and passes it every other prop.

### DialogOverlay

Renders Radix Dialog.Overlay and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |

### DialogPortal

Renders Radix Dialog.Portal and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `container` | `Element \| DocumentFragment \| null` |  | Specify a container element to portal the content into. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |

### DialogTitle

Renders Radix Dialog.Title and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### DialogTrigger

Renders Radix Dialog.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="dialog"` (Dialog), `data-slot="dialog-body"` (DialogBody), `data-slot="dialog-close"` (DialogClose), `data-slot="dialog-portal"` (DialogContent), `data-slot="dialog-description"` (DialogDescription), `data-slot="dialog-footer"` (DialogFooter), `data-slot="dialog-header"` (DialogHeader), `data-slot="dialog-overlay"` (DialogOverlay), `data-slot="dialog-portal"` (DialogPortal), `data-slot="dialog-title"` (DialogTitle), `data-slot="dialog-trigger"` (DialogTrigger), and `data-size`.

**Provider strings:** `close` (`BooleanUIProvider`'s `strings`).

## Theming

The panel is a floating surface, like menus and popovers: `--popover` and `--popover-foreground`. Its height stops below the WordPress admin bar (`--wp-admin--admin-bar--height`).

| Token | Used for |
| --- | --- |
| `--background` | background |
| `--foreground` | text |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
