# Dialog

> A modal window for completing a short task or confirming a decision.

Source: /docs/components/dialog

```tsx
import { Dialog } from '@aksornproject/aksara-ui'
```

## Basic dialog

`Dialog` is a namespace of parts. `Dialog.Trigger` opens the dialog and `Dialog.Close` closes it. Both are unstyled, so pass a library `Button` through `render`.

`Dialog.Popup` draws the scrim, centres the surface and keeps it 24px from the overlay edges. Put a `Dialog.Header`, a `Dialog.Body` and a `Dialog.Footer` inside it.

```tsx
import { Button, Dialog, Typography } from '@aksornproject/aksara-ui'

export default function DialogBasic() {
  return (
    <Dialog.Root>
      <Dialog.Trigger render={<Button>Share report</Button>} />
      <Dialog.Popup size="md">
        <Dialog.Header title="Share report" subtitle="Term 1 science grades" />
        <Dialog.Body>
          <Typography className="text-color-text-primary">
            Parents of the 32 students in M.1/2 get an email with a link to
            their child’s report.
          </Typography>
        </Dialog.Body>
        <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
          <Dialog.Close
            render={
              <Button variant="secondary-accent" size="lg">
                Cancel
              </Button>
            }
          />
          <Dialog.Close render={<Button size="lg">Share</Button>} />
        </Dialog.Footer>
      </Dialog.Popup>
    </Dialog.Root>
  )
}
```

## Sizes

Choose `sm`, `md`, `lg` or `xl` with `size`. `lg` is the default. Each size adapts to the available width and leaves at least 24px at the edges. `full` fills the overlay with no corner radius.

Sizes follow the overlay's width. If you use `container`, they adapt to that element rather than the browser window.

```tsx
import {
  Button,
  Dialog,
  type DialogSize,
  Typography,
} from '@aksornproject/aksara-ui'

const SIZES: DialogSize[] = ['sm', 'md', 'lg', 'xl', 'full']

export default function DialogSizes() {
  return (
    <>
      {SIZES.map((size) => (
        <Dialog.Root key={size}>
          <Dialog.Trigger
            render={<Button variant="secondary-accent">{size}</Button>}
          />
          <Dialog.Popup size={size}>
            <Dialog.Header title={`A ${size} dialog`} />
            <Dialog.Body>
              <Typography className="text-color-text-primary">
                The width comes from the surface modal width token for {size}.
              </Typography>
            </Dialog.Body>
            <Dialog.Footer className="flex justify-end">
              <Dialog.Close render={<Button size="lg">Done</Button>} />
            </Dialog.Footer>
          </Dialog.Popup>
        </Dialog.Root>
      ))}
    </>
  )
}
```

## Header

`title` names the dialog and clamps to two lines. `subtitle` adds a description below it. `leadingIcon` puts an icon before the title.

`info` adds a question button after the title. Its value is the tooltip content. `actions` sits between the title and the close button. `extra` adds a row under the title for badges or steps. `showClose={false}` removes the close button.

```tsx
import { IconInterfaceEssentialSetting } from '@aksornproject/aksara-icons'
import { Badge, Button, Dialog, Typography } from '@aksornproject/aksara-ui'

export default function DialogHeader() {
  return (
    <Dialog.Root>
      <Dialog.Trigger
        render={<Button variant="secondary-accent">Class settings</Button>}
      />
      <Dialog.Popup size="md">
        <Dialog.Header
          title="Class settings"
          subtitle="M.1/2 science"
          leadingIcon={<IconInterfaceEssentialSetting />}
          info="Only teachers of this class can change these settings."
          actions={
            <Button variant="tertiary-accent" size="sm">
              Reset
            </Button>
          }
          extra={<Badge>32 students</Badge>}
        />
        <Dialog.Body>
          <Typography className="text-color-text-primary">
            Choose who can join and when homework is due.
          </Typography>
        </Dialog.Body>
      </Dialog.Popup>
    </Dialog.Root>
  )
}
```

## Scrolling body and footer

The header and the footer stay put. The body takes the rest of the height and scrolls once the popup reaches the overlay height. You do not set a height. This demo caps the popup with `max-h-[480px]` to show the scroll.

`padding` on `Dialog.Body` is `default` (24px) or `flush` (none). Both keep the 16px gap. Use `flush` for rows that touch the edges.

`Dialog.Footer` is one styled `div` with the surface, the top divider and the 24 by 16 padding. It never reorders, groups or stacks its children. Arrange the buttons in JSX and `className`. The JSX order is the DOM order, the Tab order and the screen order.

```tsx
import { Badge, Button, Checkbox, Dialog } from '@aksornproject/aksara-ui'
import { useState } from 'react'

const GRADES = [
  'Primary 1',
  'Primary 2',
  'Primary 3',
  'Primary 4',
  'Primary 5',
  'Primary 6',
  'Secondary 1',
  'Secondary 2',
  'Secondary 3',
  'Secondary 4',
  'Secondary 5',
  'Secondary 6',
]

export default function DialogScrolling() {
  const [picked, setPicked] = useState<string[]>(['Primary 1'])

  return (
    <Dialog.Root>
      <Dialog.Trigger render={<Button>Filters</Button>} />
      <Dialog.Popup size="md" className="max-h-[480px]">
        <Dialog.Header
          title="Filters"
          subtitle="Grade"
          extra={<Badge>{`${picked.length} selected`}</Badge>}
        />
        <Dialog.Body padding="flush">
          {GRADES.map((grade) => (
            <div key={grade} className="px-spacing-24 py-spacing-12">
              <Checkbox
                label={grade}
                checked={picked.includes(grade)}
                onCheckedChange={(checked) =>
                  setPicked((all) =>
                    checked
                      ? [...all, grade]
                      : all.filter((entry) => entry !== grade),
                  )
                }
              />
            </div>
          ))}
        </Dialog.Body>
        <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-12">
          <div className="flex min-w-spacing-160 flex-1 items-center">
            <Button
              variant="link-accent"
              size="lg"
              disabled={picked.length === 0}
              onClick={() => setPicked([])}
            >
              Clear all
            </Button>
          </div>
          <div className="flex items-center gap-spacing-8">
            <Dialog.Close
              render={
                <Button variant="secondary-accent" size="lg">
                  Cancel
                </Button>
              }
            />
            <Dialog.Close render={<Button size="lg">Apply</Button>} />
          </div>
        </Dialog.Footer>
      </Dialog.Popup>
    </Dialog.Root>
  )
}
```

## Alert dialog

Use `Dialog.AlertRoot` for a confirmation. It gives the popup `role="alertdialog"`, is always modal and ignores presses outside. Only its buttons and Escape close it. `Dialog.Trigger` and the other parts work the same inside it.

```tsx
import { Button, Dialog, Typography } from '@aksornproject/aksara-ui'

export default function DialogAlert() {
  return (
    <Dialog.AlertRoot>
      <Dialog.Trigger
        render={<Button color="destructive">Delete class</Button>}
      />
      <Dialog.Popup size="sm">
        <Dialog.Header title="Delete M.1/2?" />
        <Dialog.Body>
          <Typography className="text-color-text-primary">
            Students will no longer see this class or its homework.
          </Typography>
        </Dialog.Body>
        <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
          <Dialog.Close
            render={
              <Button variant="secondary-accent" size="lg">
                Cancel
              </Button>
            }
          />
          <Dialog.Close
            render={
              <Button color="destructive" size="lg">
                Delete class
              </Button>
            }
          />
        </Dialog.Footer>
      </Dialog.Popup>
    </Dialog.AlertRoot>
  )
}
```

## Controlled

Pass `open` and `onOpenChange` to `Dialog.Root` to manage the state in your app, for example to close after a save.

The popup takes focus when it opens. Pass `initialFocus` to start in a field instead.

```tsx
import { Button, Dialog, TextInput, Typography } from '@aksornproject/aksara-ui'
import { useRef, useState } from 'react'

export default function DialogControlled() {
  const [open, setOpen] = useState(false)
  const [name, setName] = useState('Science M.1/2')
  const [draft, setDraft] = useState(name)
  const field = useRef<HTMLInputElement>(null)

  function openChange(next: boolean) {
    if (next) setDraft(name)
    setOpen(next)
  }

  function save() {
    setName(draft.trim() || name)
    setOpen(false)
  }

  return (
    <>
      <Typography variant="body-medium" className="text-color-text-primary">
        {name}
      </Typography>
      <Dialog.Root open={open} onOpenChange={openChange}>
        <Dialog.Trigger
          render={<Button variant="secondary-accent">Rename</Button>}
        />
        <Dialog.Popup size="sm" initialFocus={field}>
          <Dialog.Header title="Rename class" />
          <Dialog.Body>
            <TextInput
              ref={field}
              label="Class name"
              value={draft}
              onChange={(event) => setDraft(event.target.value)}
              onKeyDown={(event) => {
                if (event.key === 'Enter') save()
              }}
            />
          </Dialog.Body>
          <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
            <Dialog.Close
              render={
                <Button variant="secondary-accent" size="lg">
                  Cancel
                </Button>
              }
            />
            <Button size="lg" onClick={save}>
              Save
            </Button>
          </Dialog.Footer>
        </Dialog.Popup>
      </Dialog.Root>
    </>
  )
}
```

## Container

`container` renders the overlay inside an element instead of `<body>`. The overlay fills the element and the width tokens follow its width. Give the element a non-static `position`. Dialogs nested in a contained dialog stay inside the same element.

Menus, tooltips and nested dialogs opened inside a dialog portal into its layer, so they paint above the popup. A nested dialog draws its own backdrop.

```tsx
import { Button, Dialog, Typography } from '@aksornproject/aksara-ui'
import { useRef } from 'react'

export default function DialogContainer() {
  const stage = useRef<HTMLDivElement>(null)

  return (
    <div
      ref={stage}
      className="relative flex h-[360px] w-full max-w-[810px] items-center justify-center overflow-hidden rounded-radius-radius-8 bg-color-background-surface"
    >
      <Dialog.Root>
        <Dialog.Trigger render={<Button>Leave lesson</Button>} />
        <Dialog.Popup size="sm" container={stage}>
          <Dialog.Header title="Leave the lesson?" />
          <Dialog.Body>
            <Typography className="text-color-text-primary">
              The lesson stops on the current slide.
            </Typography>
          </Dialog.Body>
          <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
            <Dialog.Close
              render={
                <Button variant="secondary-accent" size="lg">
                  Keep teaching
                </Button>
              }
            />
            <Dialog.Close render={<Button size="lg">Leave</Button>} />
          </Dialog.Footer>
        </Dialog.Popup>
      </Dialog.Root>
    </div>
  )
}
```

## Accessibility

- The header `title` labels the popup through `aria-labelledby`. The `subtitle` describes it through `aria-describedby`.
- A popup without a `Dialog.Header` needs a `Dialog.Title` or an `aria-label`. Development builds log an error otherwise.
- The popup takes focus when it opens, so the info tooltip does not cover the title for keyboard users. The next Tab reaches the info button. Focus returns to the trigger on close.
- The close button is named "Close" and the info button "More information". Pass `closeLabel` and `infoLabel` to translate them.
- `Dialog.AlertRoot` gives `role="alertdialog"`. Use it for confirmations.

## API

### Dialog.Root

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` |  | Whether the dialog is currently open. |
| `defaultOpen` | `boolean` | `false` | Whether the dialog is initially open. To render a controlled dialog, use the `open` prop instead. |
| `modal` | `"trap-focus" \| boolean` | `true` | Determines if the dialog enters a modal state when open. - `true`: user interaction is limited to just the dialog: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled. When `modal` is `true` or `'trap-focus'`, render `<Dialog.Close>` inside `<Dialog.Popup>` so touch screen readers can escape the popup. |
| `onOpenChange` | `((open: boolean, eventDetails: DialogRoot.ChangeEventDetails) => void)` |  | Event handler called when the dialog is opened or closed. |
| `onOpenChangeComplete` | `((open: boolean) => void)` |  | Event handler called after any animations complete when the dialog is opened or closed. |
| `disablePointerDismissal` | `boolean` | `false` | Whether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it. |
| `actionsRef` | `React.RefObject<DialogRootActions \| null>` |  | A ref to imperative actions. - `unmount`: Ends the closing phase of the dialog after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the dialog completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the dialog imperatively when called. |
| `handle` | `DialogHandle<unknown>` |  | A handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method. |
| `children` | `PayloadChildRenderFunction<unknown> \| ReactNode` |  | The content of the dialog. This can be a regular React node or a render function that receives the `payload` of the active trigger. |
| `triggerId` | `string \| null` |  | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set). |
| `defaultTriggerId` | `string \| null` |  | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog. |

### Dialog.AlertRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onOpenChange` | `((open: boolean, eventDetails: AlertDialogRoot.ChangeEventDetails) => void)` |  | Event handler called when the alert dialog is opened or closed. |
| `actionsRef` | `React.RefObject<DialogRootActions \| null>` |  | A ref to imperative actions. - `unmount`: Ends the closing phase of the alert dialog after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the alert dialog completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the alert dialog imperatively when called. |
| `handle` | `AlertDialogHandle<unknown>` |  | A handle to associate the alert dialog with a trigger. If specified, allows external triggers to control the alert dialog's open state. Can be created with the AlertDialog.createHandle() method. |
| `open` | `boolean` |  | Whether the dialog is currently open. |
| `defaultOpen` | `boolean` | `false` | Whether the dialog is initially open. To render a controlled dialog, use the `open` prop instead. |
| `onOpenChangeComplete` | `((open: boolean) => void)` |  | Event handler called after any animations complete when the dialog is opened or closed. |
| `children` | `PayloadChildRenderFunction<unknown> \| ReactNode` |  | The content of the dialog. This can be a regular React node or a render function that receives the `payload` of the active trigger. |
| `triggerId` | `string \| null` |  | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set). |
| `defaultTriggerId` | `string \| null` |  | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog. |

### Dialog.Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `handle` | `DialogHandle<unknown>` |  | A handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method. |
| `payload` | `unknown` |  | A payload to pass to the dialog when it is opened. |
| `id` | `string` |  | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the dialog in controlled mode (with the DialogRoot `triggerId` prop). |
| `className` | `string \| ((state: DialogTriggerState) => string \| undefined)` |  | CSS class applied to the element, or a function that returns a class based on the component's state. |
| `render` | `ComponentRenderFn<HTMLProps, DialogTriggerState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: DialogTriggerState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |

### Dialog.Close

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogCloseState) => string \| undefined)` |  | CSS class applied to the element, or a function that returns a class based on the component's state. |
| `render` | `ComponentRenderFn<HTMLProps, DialogCloseState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: DialogCloseState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |

### Dialog.Popup

Use `className` to customise the popup. `Dialog.Popup` does not accept `render`.

| Prop | Type | Description |
| --- | --- | --- |
| `style` | `React.CSSProperties \| ((state: DialogPopupState) => React.CSSProperties \| undefined)` | Style applied to the element, or a function that returns a style object based on the component's state. |
| `initialFocus` | `boolean \| React.RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| HTMLElement \| null \| void)` | Determines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, `null` to fall back to the default behavior, or `false`/`undefined` to do nothing. |
| `finalFocus` | `boolean \| React.RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| HTMLElement \| null \| void)` | Determines the element to focus when the dialog is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, `null` to fall back to the default behavior, or `false`/`undefined` to do nothing. |
| `size` | `"full" \| "lg" \| "md" \| "sm" \| "xl"` | Defaults to `'lg'`. |
| `container` | `HTMLElement \| React.RefObject<HTMLElement \| ShadowRoot \| null> \| ShadowRoot \| null` | Renders the overlay inside this element instead of `<body>`. The overlay fills the element and the width tokens follow its width. Give the element a non-static `position`. Dialogs nested in a contained dialog stay inside the same element. |
| `className` | `string` | Classes merged last on the popup surface. |
| `children` | `ReactNode` | The Header, Body and Footer, or a Title and your own content. |

### Dialog.Header

| Prop | Type | Description |
| --- | --- | --- |
| `title` (required) | `ReactNode` | Rendered in a Base UI Title, so it names the popup. Clamps to two lines. |
| `subtitle` | `ReactNode` | Rendered in a Base UI Description, so it describes the popup. |
| `leadingIcon` | `ReactNode` | A 24px icon in a 36px box before the title. |
| `actions` | `ReactNode` | Buttons between the title and the close button. |
| `extra` | `ReactNode` | A row under the title for badges or steps. |
| `className` | `string` | Classes merged last on the header. |
| `showClose` | `boolean` | Shows the close button. Defaults to `true`. |
| `closeLabel` | `string` | Accessible name of the close button. Defaults to `'Close'`. |
| `info` | `ReactNode` | Shows a question button after the title. This is the content of its tooltip. |
| `infoLabel` | `string` | Accessible name of the question button. Defaults to `'More information'`. |

### Dialog.Title

| Prop | Type | Description |
| --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, DialogTitleState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: DialogTitleState) => React.CSSProperties \| undefined)` | Style applied to the element, or a function that returns a style object based on the component's state. |
| `className` | `string` | Classes merged last on the title. |

### Dialog.Description

| Prop | Type | Description |
| --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, DialogDescriptionState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: DialogDescriptionState) => React.CSSProperties \| undefined)` | Style applied to the element, or a function that returns a style object based on the component's state. |
| `className` | `string` | Classes merged last on the description. |

### Dialog.Body

| Prop | Type | Description |
| --- | --- | --- |
| `padding` | `"default" \| "flush"` | Defaults to `'default'`. |

### Dialog.Footer

`DialogFooterProps` is the props of a `div`, with a forwarded ref and `className` merged last. It adds no props of its own.