MenuChevron Down
Dialog - Docs - Artefact

Dialog

Overlays
Auto-interactive

Introduction

A modal window overlaid on the page that requires user interaction before continuing.

Hydration

Tier 1 — auto-interactive by default. A Dialog has no meaningful static fallback — opening, focus trapping, and ESC handling all require client-side JavaScript — so it hydrates as an island by default. Pass interactive={false} to render a static, inert dialog shell that ships no client JS.

interactive propResult
omittedHydrates as an island (default)
trueHydrates as an island
falseStatic — no client JS

All interactivity decisions in the library route through the shared shouldHydrate() helper in app/components/ui/island-utils.ts.

Accessibility

Complies with the Dialog (Modal) WAI-ARIA design pattern. When hydrated (interactive defaults to true), the dialog ships the following behavior:

  • Focus moves into the dialog on open — to initialFocusEl(), else the first focusable element, else the content itself.
  • Focus is trapped while openTab / Shift+Tab cycle only within the dialog content. Nested dialogs are handled: only the topmost dialog traps and owns Escape.
  • Escape closes the dialog (unless closeOnEscape={false}).
  • Background is inert — everything outside the dialog (including a parent dialog behind a nested one) gets inert + is removed from the tab order, while the dialog subtree stays interactive.
  • Body scroll is locked while at least one dialog is open, and restored when the last one closes.
  • Focus returns to the trigger on close (or to finalFocusEl() if provided).
  • Accessible name — derived from title (aria-labelledby) or an explicit aria-label. A client-side console.warn is emitted if neither is present.
  • role="alertdialog" — pass role="alertdialog" for destructive confirmations, and point initialFocusEl at the cancel/safe action so it receives focus first. With closeOnInteractOutside={false} the dialog can only be dismissed via a button or Escape.

Usage

Basic Dialog

import { Dialog, Button } from "../components/ui";

export default function MyPage() {
  return (
    <Dialog
      trigger={<Button>Open Dialog</Button>}
      title="Confirm action"
      description="Are you sure you want to continue?"
      body="This action cannot be undone."
      cancel={<Button variant="outline">Cancel</Button>}
      confirm={<Button>Confirm</Button>}
    />
  );
}

CMS Page Builder

This component is available as a dialog block in the Page Builder (content/pages/*.json). trigger is a nested block list (the CMS always submits an array):

{
  "type": "dialog",
  "title": "Confirm action",
  "description": "Are you sure you want to continue?",
  "confirmText": "Confirm",
  "cancelText": "Cancel",
  "trigger": [{ "type": "button", "text": "Open Dialog" }]
}

Props

Dialog

PropTypeDescription
triggerJSX.ElementElement that opens the dialog when activated.

| title | `string \ | JSX.Element` | The dialog title. | | description | `string \ | JSX.Element` | The dialog description. | | body | `string \ | JSX.Element` | The main body content. | | footer | `string \ | JSX.Element` | Custom footer content. |

| cancel | JSX.Element | Element rendered as a close (cancel) trigger. | | confirm | JSX.Element | Element rendered as an action trigger. | | closable | boolean | Whether to show the close button. Default: true. | | interactive | boolean | Enable client-side hydration for interactive behavior. | | class | string | Custom CSS classes for the root element. |

| role | `"dialog" \ | "alertdialog"` | Dialog variant. Use "alertdialog" for destructive confirmations. Default: "dialog". |

| aria-label | string | Accessible name when no title is provided. | | closeOnEscape | boolean | Close when Escape is pressed. Default: true. | | closeOnInteractOutside | boolean | Close when the backdrop is clicked. Default: true. |

| initialFocusEl | `() => HTMLElement \ | null` | Element to focus on open. Defaults to the first focusable. | | finalFocusEl | `() => HTMLElement \ | null` | Element to focus on close. Defaults to the trigger. |

Additional props (e.g. open, defaultOpen, onOpenChange, id) are forwarded to the underlying dialog primitive.