# Dialog

A window on top of the page for one task or one form.

```tsx
import {
  Button,
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  Field,
  Input,
} from 'ferry-ui'

export default function DialogHero() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Rename project</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Rename project</DialogTitle>
          <DialogDescription>The new name shows in the list of projects.</DialogDescription>
        </DialogHeader>
        <DialogBody>
          <Field label="Name">
            <Input defaultValue="Billing portal" />
          </Field>
        </DialogBody>
        <DialogFooter>
          <DialogClose asChild>
            <Button>Cancel</Button>
          </DialogClose>
          <DialogClose asChild>
            <Button variant="primary">Save</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

## Usage guidelines

- **One task for each dialog.** A dialog asks for one decision or shows one short form.
- **A confirmation is not a dialog.** To confirm a destructive action, use [Confirm Dialog](/docs/components/confirm-dialog) or [Alert Dialog](/docs/components/alert-dialog).
- **Keep the page in view with a sheet.** For a side panel, use [Sheet](/docs/components/sheet).
- **Give each dialog a title.** `DialogTitle` is the accessible name of the dialog.

## Anatomy

Import the parts and put them together.

```tsx title="Anatomy"

  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from 'ferry-ui'

<Dialog>
  <DialogTrigger />
  <DialogContent>
    <DialogHeader>
      <DialogTitle />
      <DialogDescription />
    </DialogHeader>
    <DialogBody />
    <DialogFooter>
      <DialogClose />
    </DialogFooter>
  </DialogContent>
</Dialog>
```

| Part | Role |
| --- | --- |
| `Dialog` | Holds the open state. |
| `DialogTrigger` | Opens the dialog on a click. |
| `DialogContent` | The panel. It adds the overlay and the close button. |
| `DialogHeader` | Holds the title and the description. |
| `DialogBody` | The content. It is the only part that scrolls. |
| `DialogFooter` | The actions. Cancel comes first, the primary action comes last. |
| `DialogClose` | Closes the dialog on a click. |

## Examples

### Open state

A dialog holds its open state by default. Use `defaultOpen` to open it at the start.

To control the state, pass `open` and `onOpenChange`. Use this to close the dialog after a submit.

```tsx
import * as React from 'react'
import {
  Button,
  Dialog,
  DialogBody,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  Field,
  Input,
  toast,
} from 'ferry-ui'

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

  function invite(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    toast.success('Invitation sent')
    // The form is done: close the dialog from the code.
    setOpen(false)
  }

  return (
    <>
      <Button onClick={() => setOpen(true)}>Invite a member</Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <form onSubmit={invite}>
            <DialogHeader>
              <DialogTitle>Invite a member</DialogTitle>
              <DialogDescription>The member gets an email with a link.</DialogDescription>
            </DialogHeader>
            <DialogBody>
              <Field label="Email">
                <Input type="email" required placeholder="maya@example.com" />
              </Field>
            </DialogBody>
            <DialogFooter>
              <Button onClick={() => setOpen(false)}>Cancel</Button>
              <Button type="submit" variant="primary">
                Send invitation
              </Button>
            </DialogFooter>
          </form>
        </DialogContent>
      </Dialog>
    </>
  )
}
```

### Sizes

The `size` prop of `DialogContent` sets the maximum width.

```tsx
import {
  Button,
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from 'ferry-ui'

const SIZES = ['sm', 'md', 'lg', 'xl', 'xxl'] as const

export default function DialogSizes() {
  return (
    <>
      {SIZES.map((size) => (
        <Dialog key={size}>
          <DialogTrigger asChild>
            <Button>{size}</Button>
          </DialogTrigger>
          <DialogContent size={size}>
            <DialogHeader>
              <DialogTitle>Size {size}</DialogTitle>
              <DialogDescription>The panel takes this width on a screen that is wide enough.</DialogDescription>
            </DialogHeader>
            <DialogBody>
              <p className="text-[13px] text-foreground-light">On a phone, the panel takes the width of the screen.</p>
            </DialogBody>
            <DialogFooter>
              <DialogClose asChild>
                <Button variant="primary">Done</Button>
              </DialogClose>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      ))}
    </>
  )
}
```

| Size | Width |
| --- | --- |
| `sm` | 384px |
| `md` | 448px (default) |
| `lg` | 512px |
| `xl` | 672px |
| `xxl` | 896px |

### Long content

Put long content in `DialogBody`. The body scrolls. The header and the footer stay in view.

```tsx
import {
  Button,
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from 'ferry-ui'

const CHANGES = [
  'Invoices show the tax for each line.',
  'Members can have more than one role.',
  'API keys can expire on a date.',
  'The audit log keeps 90 days of events.',
  'Projects can move to another workspace.',
  'Exports include the archived orders.',
  'The search finds customers by email.',
  'Webhooks send a new event for refunds.',
  'The dashboard shows the usage for each day.',
  'Notifications have a weekly summary.',
  'The billing page lists the past payments.',
  'Each order shows its delivery status.',
]

export default function DialogScroll() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Release notes</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Release notes</DialogTitle>
          <DialogDescription>The changes of this month.</DialogDescription>
        </DialogHeader>
        <DialogBody className="max-h-64">
          <ul className="flex flex-col gap-3 text-[13px] text-foreground-light">
            {CHANGES.map((change) => (
              <li key={change}>{change}</li>
            ))}
          </ul>
        </DialogBody>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="primary">Done</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

### No description

A dialog with no `DialogDescription` must tell that to screen readers. Pass `aria-describedby={undefined}` to `DialogContent`.

```tsx
import { Button, Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle, DialogTrigger, Kbd, useModKey } from 'ferry-ui'

export default function DialogNoDescription() {
  const mod = useModKey()
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="ghost">Keyboard shortcuts</Button>
      </DialogTrigger>
      <DialogContent size="sm" aria-describedby={undefined}>
        <DialogHeader>
          <DialogTitle>Keyboard shortcuts</DialogTitle>
        </DialogHeader>
        <DialogBody>
          <p className="flex items-center justify-between text-[13px] text-foreground-light">
            Open the command menu <Kbd>{mod} K</Kbd>
          </p>
          <p className="flex items-center justify-between text-[13px] text-foreground-light">
            Close a dialog <Kbd>Esc</Kbd>
          </p>
        </DialogBody>
      </DialogContent>
    </Dialog>
  )
}
```

## Accessibility

- The focus moves into the dialog when it opens. It goes back to the trigger when the dialog closes.
- <Kbd>Esc</Kbd> closes the dialog. A click outside the panel also closes it.
- `closeLabel` sets the accessible name of the close button. The default is "Close".

## API reference

Each part also accepts the props of its Radix UI primitive and the attributes of its element.

### Dialog

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `open` | `boolean` |  |  |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  |  |
| `modal` | `boolean` |  |  |

### DialogTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### DialogContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "md" \| "lg" \| "xl" \| "xxl"` | `md` | Max width from the `sm` breakpoint up (on phones the panel is full width with a 1rem gutter on each side): `sm` 384px, `md` 448px (default, simple forms), `lg` 512px, `xl` 672px (multi-column forms), `xxl` 896px (tables, previews). |
| `showCloseButton` | `boolean` | `true` | Renders the top-right close (X) button. Default `true`; when `false`, give the footer a way out. |
| `closeLabel` | `string` | `Close` | Accessible name of the built-in close (X) button. Default `"Close"`; pass a translation in localized apps. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `asChild` | `boolean` |  |  |
| `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. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. 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. |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |

### DialogHeader

`DialogHeader` has no props of its own. It accepts the attributes of the element it renders.

### DialogTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### DialogDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### DialogBody

`DialogBody` has no props of its own. It accepts the attributes of the element it renders.

### DialogFooter

`DialogFooter` has no props of its own. It accepts the attributes of the element it renders.

### DialogClose

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
