Dialog
Holds the page for a short task or a choice that must finish first. A grey shell holding one white card (the title, the X and the content), with the actions on the grey.
Examples
Sizes
sm
defaultdefault
lg
States
rest
hover
focus
active
disabled
Usage
When to use it
- Use
- The person must finish or abandon a short task before the page makes sense again: rename, create a key, edit a secret.
- Use
- An action needs one yes or no and the page should not change until it gets one: stop a run, revert a prompt release.
- Use
- The action cannot be undone and deletes something with a name: an agent, a member, a key.
- Not for
- The task takes more than one screen of fields, or the page must stay beside it. Use Sheet.
- Not for
- A few lines next to a control and the page stays usable. Use Popover.
- Not for
- The message reports a result and needs no answer, or the action is undoable (toast with Undo). Use Toast.
Rules
12 rules
- Must
- Pass open and onOpenChange; never render the dialog conditionally. Because defaultOpen is read once, so after Escape the parent still holds editing and the dialog will not open again.
- Must
- Return the server promise from onAction; never close in onClick. Because closing in onClick drops a failed save without a word, where onAction holds the dialog pending and shows the error.
- Never
- Set defaultAction="primary" beside a destructive action; leave it unset or "dismiss". Because Enter outside a field would then press Delete.
- Must
- Use role="alertdialog" with confirmText and a DialogConfirmField to delete for good a thing that holds others, such as an agent, a workspace or a data source; one record, such as a prompt release or a run, takes a plain alertdialog. Because a stray click or Enter deletes it for good, and typing the name makes the person read which one, while typing for every record trains them to type through the real one.
- Never
- Confirm an undoable action; do it and show a toast with Undo. Because a confirm on every undoable action trains people to click through, so they click through the real one too.
- Must
- Title a dialog in a few words; put the explanation in DialogDescription. With one field, the title names it: give the field an aria-label, not a second visible title. Because the title is a 16px heading on the X's line, so a sentence wraps beside the X and pushes the body down.
- Must
- Title an alert as a question ("Stop run 4812?") and work as a verb phrase ("Rename agent"); never "Are you sure?". Because "Are you sure?" names neither the action nor the object, so a screen reader opening it hears no context.
- Should
- Repeat the title verb on the primary ("Stop run") and label the dismiss with what it keeps or "Cancel".
- Should
- State the consequence with numbers: what is lost, what keeps working, for how long.
- Must
- Give every dialog a DialogTitle, with className="sr-only" when the design shows none. Because Base UI names the dialog from DialogTitle, so without one a screen reader announces only "dialog".
- Should
- Let the footer decide focus on open, or mark one element data-autofocus; never focus the panel yourself.
- Never
- Restyle the backdrop or shadow; pass only width or layout classes. Because the 24% backdrop and the frame line in shadow-overlay are shared by every overlay, so a custom one stands apart from Sheet and Popover.
Accessibility
6 checks
- The panel is role="dialog" or "alertdialog" named by DialogTitle, described by DialogDescription, with aria-modal.
- Focus lands from the footer default or data-autofocus; Tab cycles inside; the page behind is inert.
- Escape closes this dialog only and does nothing while pending.
- Enter presses the footer default; in a typed-confirm field it does nothing.
- A failed action shows a role="alert" message above the footer.
- The corner X is a button named "Close", disabled while pending.
Edge cases
12 edge cases
- 112-character title and 300-character description
- Wrap inside 400px; the X keeps its corner.
- A long ID to name
- Title it by the readable name ("Delete refund-prompt v14?") and put the ID in DialogDescription in code; an ID in the title breaks mid-word.
- The deleted row is gone
- Pass DialogContent finalFocus the next row's actions button, else the previous row's, else the table, since the trigger left with the row.
- 40 rows at 390x640
- The panel stops at 100dvh minus 64px and scrolls inside; past 3 screens use a Sheet.
- Right to left
- The X moves to the top left and the footer reverses.
- 10-second action
- The button spins; Cancel, the X, Escape and the backdrop do nothing.
- Action rejects
- The dialog stays open with the typed name and an alert; a retry clears it first.
- Popover, menu and child dialog inside
- Escape closes the innermost only; the parent dims under its own overlay.
- Zoom 200% or 320px
- The title wraps and the panel scrolls; the footer buttons wrap and stay at the end.
- Touch
- Buttons stay at the end at their own width and grow to 44px; focus starts on the panel so the keyboard stays down.
- Forced colors
- The panel gets a 1px CanvasText outline.
- Reduced motion
- Open and close cut; a running action's arc stops turning but stays drawn.
API
11 props
Dialog role'dialog' | 'alertdialog'default'dialog'Dialog confirmTextstringDialog open, defaultOpen, onOpenChangeBase UIdefaultfalseDialogContent size'sm' | 'default' | 'lg'default'default'DialogContent showCloseButtonbooleandefaultrole === dialogDialogContent initialFocus, finalFocusRefObject<HTMLElement | null> | ((type) => HTMLElement | boolean | null) | booleandefaultfirst control, the triggerDialogFooter defaultAction'primary' | 'dismiss' | 'none'defaultautoDialogAction onAction() => void | PromiseDialogAction closebooleandefaulttrue with onActionDialogActionButtonPropsDialogConfirmField label(name) => ReactNodedefault"Type <name> to confirm"
Tokens
25 tokens
--surface-overlay- oklch(var(--ink-c) / 0.24)
--surface-page- var(--paper)
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--radius-frame- 10px
--overlay-sm- 400px
--overlay- 480px
--overlay-lg- 640px
--gutter-sm- 16px
--space-8- 32px
--dur-slow- 180ms
--dur-base- 160ms
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--type-label- 400 14px/16px var(--font-sans)
--type-body-sm- 380 13px/18px var(--font-sans)
--type-tag- var(--type-code-sm)
--text-1- var(--ink)
--text-2- oklch(var(--ink-c) / 0.80)
--text-3- oklch(var(--ink-c) / 0.66)
--line-row- oklch(var(--ink-c) / 0.08)
--line-control- oklch(var(--ink-c) / 0.16)
--accent-mark- var(--ink)
--accent-wash- var(--surface-selected)
--accent-line- var(--ink)
--dur-spin- 800ms
--ease-spin- linear