# Dialog

> A modal overlay that interrupts the page for a focused task

Status: stable. Added in Components 2.7.0. Updated in Components 2.14.0, 23 Sept 2026.

A modal overlay that interrupts the page for a focused task, built on Base UI's dialog primitive.

## Import

```tsx
import { Dialog } from '@oztix/roadie-components/dialog'
```

## Examples

Each example leads with an [IconTile](https://ticketsolutionsptyltd.github.io/roadie/components/icon-tile.md) (`size='2xl' shape='circle'`, its `intent` inherited from the dialog) centred above the title. `Dialog.Header` handles the centring.

### Default

A trigger opens a centred modal over a dimmed backdrop. `Dialog.Content` collapses the underlying `Portal`, `Backdrop`, `Viewport`, and `Popup` into a single element.

```tsx
<Dialog>
  <Dialog.Trigger render={<Button>Open dialog</Button>} />
  <Dialog.Content>
    <Dialog.Header>
      <IconTile size='2xl' shape='circle'>
        <Envelope weight='duotone' />
      </IconTile>
      <Dialog.Title>Subscribe</Dialog.Title>
      <Dialog.Description>Get product updates in your inbox.</Dialog.Description>
    </Dialog.Header>
    <Dialog.Footer>
      <Dialog.Close render={<Button>Cancel</Button>} />
      <Button intent='accent' emphasis='strong'>Subscribe</Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>
```

### Emphasis

`emphasis` on the root sets how much the dialog takes over the page behind it. It defaults to `normal`, which dims and blurs the page, because a dialog asks for the user's full attention. `subtle` tints the page and leaves it readable, and `subtler` leaves it clear.

```tsx
{['normal', 'subtle', 'subtler'].map((emphasis) => (
  <Dialog key={emphasis} emphasis={emphasis}>
    <Dialog.Trigger render={<Button>{emphasis}</Button>} />
    <Dialog.Content size='sm'>
      <Dialog.Header>
        <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
        <Dialog.Title>Emphasis {emphasis}</Dialog.Title>
        <Dialog.Description>The page behind stays as visible as this level allows.</Dialog.Description>
      </Dialog.Header>
    </Dialog.Content>
  </Dialog>
))}
```

### Sizes

Use the `size` prop on `Dialog.Content` to set the maximum width. Defaults to `md`.

```tsx
<Dialog>
  <Dialog.Trigger render={<Button>Small</Button>} />
  <Dialog.Content size='sm'>
    <Dialog.Header>
      <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
      <IconTile size='2xl' shape='circle'>
        <Info weight='duotone' />
      </IconTile>
      <Dialog.Title>Small dialog</Dialog.Title>
      <Dialog.Description>max-w-sm</Dialog.Description>
    </Dialog.Header>
  </Dialog.Content>
</Dialog>
<Dialog>
  <Dialog.Trigger render={<Button>Medium</Button>} />
  <Dialog.Content size='md'>
    <Dialog.Header>
      <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
      <IconTile size='2xl' shape='circle'>
        <Info weight='duotone' />
      </IconTile>
      <Dialog.Title>Medium dialog</Dialog.Title>
      <Dialog.Description>max-w-md (default)</Dialog.Description>
    </Dialog.Header>
  </Dialog.Content>
</Dialog>
<Dialog>
  <Dialog.Trigger render={<Button>Large</Button>} />
  <Dialog.Content size='lg'>
    <Dialog.Header>
      <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
      <IconTile size='2xl' shape='circle'>
        <Info weight='duotone' />
      </IconTile>
      <Dialog.Title>Large dialog</Dialog.Title>
      <Dialog.Description>max-w-lg</Dialog.Description>
    </Dialog.Header>
  </Dialog.Content>
</Dialog>
```

### Intents

Portaled popups sit outside the trigger's CSS cascade, so set the colour context directly on `Dialog.Content` with the `intent` prop. Each intent fits a different moment:

- `info` and `success` for informational modals (dismiss-only, with a header close)
- `warning` and `danger` for caution and destructive confirmations
- `accent` to promote an action

```tsx
<Dialog>
  {/* Info for an informational, dismiss-only modal */}
  <Dialog.Trigger render={<IconButton aria-label='Release notes' emphasis='subtle'><Info weight='bold' /></IconButton>} />
  <Dialog.Content intent='info' size='sm'>
    <Dialog.Header>
      <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
      <IconTile size='2xl' shape='circle'>
        <Info weight='duotone' />
      </IconTile>
      <Dialog.Title>What's new in 2.0</Dialog.Title>
      <Dialog.Description>Saved views, faster search, and a redesigned inbox.</Dialog.Description>
    </Dialog.Header>
  </Dialog.Content>
</Dialog>

<Dialog>
  {/* Success for a dismiss-only confirmation */}
  <Dialog.Trigger render={<Button>Finish setup</Button>} />
  <Dialog.Content intent='success' size='sm'>
    <Dialog.Header>
      <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
      <IconTile size='2xl' shape='circle'>
        <CheckCircle weight='duotone' />
      </IconTile>
      <Dialog.Title>You're all set</Dialog.Title>
      <Dialog.Description>Your workspace is ready to go.</Dialog.Description>
    </Dialog.Header>
  </Dialog.Content>
</Dialog>

<Dialog role='alertdialog'>
  {/* Warning for a caution confirm */}
  <Dialog.Trigger render={<Button>Save and exit</Button>} />
  <Dialog.Content intent='warning' size='sm'>
    <Dialog.Header>
      <IconTile size='2xl' shape='circle'>
        <Warning weight='duotone' />
      </IconTile>
      <Dialog.Title>Discard changes?</Dialog.Title>
      <Dialog.Description>You have unsaved edits that will be lost.</Dialog.Description>
    </Dialog.Header>
    <Dialog.Footer>
      <Dialog.Close render={<Button>Keep editing</Button>} />
      <Dialog.Close render={<Button intent='warning' emphasis='strong'>Discard</Button>} />
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>

<Dialog role='alertdialog'>
  {/* Danger for a destructive confirm */}
  <Dialog.Trigger render={<Button intent='danger'>Remove member</Button>} />
  <Dialog.Content intent='danger' size='sm'>
    <Dialog.Header>
      <IconTile size='2xl' shape='circle'>
        <Trash weight='duotone' />
      </IconTile>
      <Dialog.Title>Remove Jordan?</Dialog.Title>
      <Dialog.Description>They lose access to this workspace immediately.</Dialog.Description>
    </Dialog.Header>
    <Dialog.Footer>
      <Dialog.Close render={<Button>Cancel</Button>} />
      <Dialog.Close render={<Button intent='danger' emphasis='strong'>Remove</Button>} />
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>

<Dialog>
  {/* Accent to promote an upgrade */}
  <Dialog.Trigger render={<Button intent='accent' emphasis='strong'>Upgrade</Button>} />
  <Dialog.Content intent='accent' size='sm'>
    <Dialog.Header>
      <IconTile size='2xl' shape='circle'>
        <Star weight='duotone' />
      </IconTile>
      <Dialog.Title>Upgrade to Pro</Dialog.Title>
      <Dialog.Description>Unlock unlimited projects and priority support.</Dialog.Description>
    </Dialog.Header>
    <Dialog.Footer>
      <Dialog.Close render={<Button>Maybe later</Button>} />
      <Button intent='accent' emphasis='strong'>Upgrade now</Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>
```

### Alert dialog

Set `role='alertdialog'` on the root for a blocking message that demands a
response. Examples are a session timeout, a cart-expiry warning or an
irreversible confirmation. This
exposes the surface as `role="alertdialog"` and raises the backdrop and surface to
the [`z-alert`](https://ticketsolutionsptyltd.github.io/roadie/foundations/elevation.md) tier so the dialog stacks **above** an
already-open dialog or drawer. The default `role='dialog'` stays on the
`z-overlay` / `z-modal` tiers.

```tsx
<Dialog role='alertdialog' open={expired} onOpenChange={onClose}>
  <Dialog.Content size='sm'>
    <Dialog.Header>
      <Dialog.Title>Your cart has expired</Dialog.Title>
    </Dialog.Header>
    <Dialog.Footer>
      <Button emphasis='strong' onClick={onClose}>
        Start over
      </Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>
```

### Composition

Dialog is a compound component. Use `Dialog.Header`, `Dialog.Body`, and `Dialog.Footer` to structure the modal, and `Dialog.Close` to dismiss it.

```tsx
<Dialog>
  <Dialog.Trigger render={<Button>View details</Button>} />
  <Dialog.Content>
    <Dialog.Header>
      <Dialog.Close render={<IconButton aria-label='Close' emphasis='normal' size='sm' className='absolute left-0 top-0'><X weight='bold' /></IconButton>} />
      <IconTile size='2xl' shape='circle'>
        <Ticket weight='duotone' />
      </IconTile>
      <Dialog.Title>Order #1024</Dialog.Title>
      <Dialog.Description>
        Placed on{' '}
        <DateTime
          at={new Date('2026-06-14T04:30:00Z')}
          timeZone='Australia/Brisbane'
          dateStyle='medium'
        />
      </Dialog.Description>
    </Dialog.Header>
    <Dialog.Body>
      <p className='text-sm text-subtle text-center'>Two tickets to the Saturday show, collected at the door.</p>
    </Dialog.Body>
  </Dialog.Content>
</Dialog>
```

### With footer actions

A `Dialog.Footer` holds a centered actions row. Combine it with `intent='danger'` for a confirm-deletion flow.

```tsx
<Dialog role='alertdialog'>
  <Dialog.Trigger render={<Button intent='danger' emphasis='strong'>Delete account</Button>} />
  <Dialog.Content intent='danger' size='sm'>
    <Dialog.Header>
      <IconTile size='2xl' shape='circle'>
        <Warning weight='duotone' />
      </IconTile>
      <Dialog.Title>Delete account?</Dialog.Title>
      <Dialog.Description>
        This permanently removes your account and cannot be undone.
      </Dialog.Description>
    </Dialog.Header>
    <Dialog.Footer>
      <Dialog.Close render={<Button>Cancel</Button>} />
      <Dialog.Close render={<Button intent='danger' emphasis='strong'>Delete</Button>} />
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>
```

## Guidelines

Use a dialog for tasks that need the user's full attention, such as confirmations, short forms, and destructive actions. Keep content concise, and set `intent` on `Dialog.Content`, not the trigger, since portaled popups do not inherit the trigger's CSS cascade. Write the title, description, and buttons with [Interface copy](https://ticketsolutionsptyltd.github.io/roadie/content/interface-copy.md#confirmations).

### Use alertdialog for blocking or destructive confirmations

**Do**

```tsx
<Dialog role='alertdialog'>
  <Dialog.Content intent='danger' size='sm'>
    <Dialog.Title>Delete event?</Dialog.Title>
    …
  </Dialog.Content>
</Dialog>
```

Set `role='alertdialog'` when the user must resolve the dialog before moving on. Examples are deleting data, discarding edits, or a session or cart expiry. Screen readers announce it assertively, and it stacks above an already-open dialog or drawer on the `z-alert` tier.

**Don’t**

```tsx
<Dialog>
  <Dialog.Content intent='danger' size='sm'>
    <Dialog.Title>Delete event?</Dialog.Title>
    …
  </Dialog.Content>
</Dialog>
```

Don't leave a destructive or blocking confirmation as the default `role='dialog'`. It is announced as a plain dialog and can be obscured by an open drawer. Keep `role='dialog'` for informational or optional content.

### Use one prominent action, the rest normal

**Do**

```tsx
<><Button>Cancel</Button><Button intent='accent' emphasis='strong'>Save changes</Button></>
```

Give the dialog one prominent button. Use `emphasis='strong'` with an `accent` intent, or `danger` for a destructive task. Leave the rest as default buttons. The primary path stays obvious at a glance.

**Don’t**

```tsx
<><Button emphasis='subtle'>Cancel</Button><Button intent='accent' emphasis='strong'>Save</Button><Button intent='accent' emphasis='strong'>Save and close</Button></>
```

Never use `emphasis='subtle'` for footer actions, and never give a dialog more than one accent or strong button. Competing prominent buttons make the user stop and weigh every option instead of following the intended path.

### Dismiss with a header close, not a lone Cancel

**Do**

```tsx
<IconButton aria-label='Close' emphasis='normal'><XIcon weight='bold' /></IconButton>
```

When the only action is to close, put a Close icon button in the top-left of the header and skip the footer entirely. The dialog reads as informational, not a decision awaiting a choice.

**Don’t**

```tsx
<Button>Cancel</Button>
```

A lone Cancel in the footer wastes the prominent action slot and adds a step. There is nothing to cancel. The user only wants to close.

### Lead notable dialogs with a matching icon

**Do**

For confirmations and notable moments, centre a `size='2xl' shape='circle'` [IconTile](https://ticketsolutionsptyltd.github.io/roadie/components/icon-tile.md) above the title and leave its `intent` unset so it inherits the dialog's. That gives a danger or warning tile for caution, and success for completion. It signals the nature of the action before the title is read.

**Don’t**

Don't give the tile an `intent` that clashes with the dialog, shrink it until it reads as decoration, or add one to routine dialogs where it is just noise.

## Accessibility

- **Keyboard**: `Escape` closes the dialog and returns focus to the trigger. Focus is trapped within the dialog while it is open.
- **Default action**: `Enter` activates the strong primary action (the `emphasis='strong'` button), like a form's default submit. It is ignored while focus is on another button, a multi-line textarea, or editable content so those keep their own `Enter` behaviour.
- **Focus**: Focus moves into the dialog on open and is restored to the trigger on close.
- **ARIA**: The popup is exposed with `role="dialog"` and `aria-modal`, with `Dialog.Title`/`Dialog.Description` providing the accessible name and description. Set `role='alertdialog'` for a blocking message that needs a response. This also raises the dialog to the `z-alert` tier so it stacks above an open dialog or drawer.
- **Icon-only close**: When the dismiss control shows only an icon, give it an explicit label so screen readers announce its purpose. For example, `<Dialog.Close render={<IconButton aria-label='Close'><XIcon weight='bold' /></IconButton>} />`.

## API reference

### Dialog

```tsx
import { Dialog } from '@oztix/roadie-components/dialog'
```

- `open`: `boolean`. Whether the dialog is currently open.
- `defaultOpen`: `boolean`. Defaults to `false`. Whether the dialog is initially open. To render a controlled dialog, use the `open` prop instead.
- `modal`: `boolean | "trap-focus"`. Defaults to `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: DialogRootChangeEventDetails) => 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`. Defaults to `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`: `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`: `ReactNode | PayloadChildRenderFunction<unknown>`. 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.
- `role`: `"dialog" | "alertdialog"`. Defaults to `'dialog'`. `'alertdialog'` raises the dialog to the `z-alert` tier so it stacks above an open modal or drawer.
- `emphasis`: `"normal" | "subtle" | "subtler"`. Defaults to `'normal'`. How much the dialog takes over the page behind it.

### Dialog.Backdrop

- `forceRender`: `boolean`. Defaults to `false`. Whether the backdrop is forced to render even when nested.
- `className`: `string | ((state: DialogBackdropState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Dialog.Body

No props beyond the standard HTML attributes.

### Dialog.Close

- `nativeButton`: `boolean`. Defaults to `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>`).
- `className`: `string | ((state: DialogCloseState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Dialog.Content

- `initialFocus`: `boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)`. 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 | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)`. 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.
- `className`: `string | ((state: DialogPopupState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.
- `intent`: `"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info" | null`.
- `size`: `"sm" | "md" | "lg" | null`.

### Dialog.Description

- `className`: `string | ((state: DialogDescriptionState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Dialog.Footer

No props beyond the standard HTML attributes.

### Dialog.Header

No props beyond the standard HTML attributes.

### Dialog.Popup

- `initialFocus`: `boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)`. 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 | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)`. 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.
- `className`: `string | ((state: DialogPopupState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.
- `intent`: `"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info" | null`.
- `size`: `"sm" | "md" | "lg" | null`.

### Dialog.Portal

- `keepMounted`: `boolean`. Defaults to `false`. Whether to keep the portal mounted in the DOM while the popup is hidden.
- `container`: `HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null`. A parent element to render the portal element into.
- `className`: `string | ((state: DialogPortalState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Dialog.Title

- `className`: `string | ((state: DialogTitleState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Dialog.Trigger

- `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).
- `nativeButton`: `boolean`. Defaults to `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>`).
- `className`: `string | ((state: DialogTriggerState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Dialog.Viewport

- `className`: `string | ((state: DialogViewportState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

Docs updated 19 Oct 2026.
