Dialog
A modal dialog on the native <dialog> element, for a confirmation, a short form or a message that needs an answer.
DialogExample.tsx
import { useRef, useState } from "react";
import { Button } from "@/components/Button";
import {
Dialog,
DialogActions,
DialogBody,
DialogButton,
type DialogResult,
} from "@/components/Dialog";
export function DialogExample() {
const [open, setOpen] = useState(false);
const [result, setResult] = useState<DialogResult | null>(null);
// The least destructive action takes focus first (APG alertdialog).
const keep = useRef<HTMLButtonElement>(null);
return (
<>
<Button
iconStart="trash"
onClick={() => {
setOpen(true);
}}
>
Delete theme
</Button>
<Dialog
open={open}
onClose={() => {
setOpen(false);
}}
onComplete={setResult}
role="alertdialog"
title="Delete “Ocean”?"
initialFocus={keep}
>
<DialogBody>
<p>The theme is removed from this browser. This can’t be undone.</p>
</DialogBody>
<DialogActions>
<DialogButton ref={keep} result="failure">
Keep theme
</DialogButton>
<DialogButton variant="primary" result="success">
Delete
</DialogButton>
</DialogActions>
</Dialog>
<p role="status">
{result === "success" && "Deleted “Ocean”."}
{result === "failure" && "Kept “Ocean”."}
</p>
</>
);
}Examples
Form
Full size
No backdrop close
Long body
Props
Any native prop not listed here goes to the root element unchanged.
DialogBody takes a <div>'s props; the dialog is described by it. DialogButton takes every Button prop. Native <dialog> props go to the dialog element, and native <div> props to DialogBody and DialogActions.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Required. Shows the dialog as a modal while true. Controlled: pair with onClose. |
onClose | () => void | — | Required. Called on any close: Escape, a backdrop click, the Close button or a DialogButton. Set open to false here. |
onComplete | (result: "success" | "failure") => void | — | Called with a DialogButton's result when one with a result is chosen, just before the dialog closes. |
title | ReactNode | — | Required. The heading at the top, and the dialog's accessible name. |
titleIcon | IconName | — | A decorative icon before the title. |
titleLevel | 2 | 3 | 4 | 5 | 6 | 2 | The title's heading level, from the page outline. It always looks like an h4. |
size | "default" | "full" | "default" | "default" is as wide as a readable line, at most. "full" fills a narrow viewport, and leaves a margin round it from 48em. |
closeOnBackdrop | boolean | true | Closes on a click on the dimmed page. A press that starts inside the dialog doesn't count. |
closeOnEsc | boolean | true | Closes on Escape. Browsers may still close on a second Escape, so don't rely on it. |
role | "dialog" | "alertdialog" | "dialog" | "alertdialog" for an urgent message that needs a response, such as a confirmation. |
initialFocus | RefObject<HTMLElement | null> | — | The element focused on open. Default: the first focusable element after the header, else Close. |
children | ReactNode | — | The content under the header: usually a DialogBody, then DialogActions. |
| Prop | Type | Default | Description |
|---|---|---|---|
justify | "start" | "end" | "center" | "space-between" | "end" | Where the actions sit along the row. They wrap on narrow screens. |
| Prop | Type | Default | Description |
|---|---|---|---|
result | "success" | "failure" | — | Reported through the dialog's onComplete. Without one, the button only closes the dialog, after its own onClick. |
Component tokens
| Token | Copy | Description | Aliases | Light | Dark | Preview |
|---|---|---|---|---|---|---|
--component-dialog-background | Dialog surface. | color.surface.raised | #fffffffrom color.gray.0 | #18202efrom color.slate.900 | ||
--component-dialog-text | Dialog text. | color.text.default | #17202bfrom color.gray.900 | #fffffffrom color.gray.0 | ||
--component-dialog-border | Surface outline. Decorative: the scrim and shadow already set the dialog apart. | color.border.default | #adbac8from color.gray.300 | #63676ffrom color.slate.500 | ||
--component-dialog-border-width | Surface outline width. | effect.border.width | 2pxfrom effect.line.2 | 2pxfrom effect.line.2 | ||
--component-dialog-radius | Surface corner radius (square when a full dialog fills a narrow viewport). | radius.surface | 4pxfrom radius.4 | 4pxfrom radius.4 | ||
--component-dialog-shadow | Surface shadow: the dialog floats above the page. | effect.elevation.overlay | 0px 4px 12px 0px rgb(0% 0% 0% / 0.12)from effect.shadow.md | 0px 4px 12px 0px rgb(0% 0% 0% / 0.4)from effect.shadow.md-dark | ||
--component-dialog-padding | Padding around the header, body and actions. | space.inset.lg | 1.5remfrom space.6 | 1.5remfrom space.6 | ||
--component-dialog-gap | Space between the header, body and actions, and between actions. | space.stack.md | 1remfrom space.4 | 1remfrom space.4 | ||
--component-dialog-backdrop | Scrim over the page behind a modal dialog. | color.fill.scrim | rgba(14, 20, 27, 0.4)from color.overlay.gray-950-40 | rgba(14, 20, 27, 0.6)from color.overlay.gray-950-60 | ||
--component-dialog-max-width | Widest default dialog: a readable line length for its text. | type.measure.text | 65chfrom type.line-length.65 | 65chfrom type.line-length.65 | No preview | |
--component-dialog-duration | Fade in and out (0ms under reduced motion). | motion.duration.base | 250msfrom motion.time.250Reduced motion: 0ms | 250msfrom motion.time.250 | No preview | |
--component-dialog-easing-enter | Easing while opening. | motion.easing.enter | cubic-bezier(0, 0, 0.2, 1)from motion.curve.out | cubic-bezier(0, 0, 0.2, 1)from motion.curve.out | No preview | |
--component-dialog-easing-exit | Easing while closing. | motion.easing.exit | cubic-bezier(0.4, 0, 1, 1)from motion.curve.in | cubic-bezier(0.4, 0, 1, 1)from motion.curve.in | No preview |
Accessibility
- A native <dialog> opened with showModal(), so the browser puts it in the top layer, makes the rest of the page inert and handles Escape. The page behind doesn't scroll while it's open.
- Named by its title, which the type requires, and described by its DialogBody when it has one. Use role alertdialog for an urgent message that needs a response.
- Focus moves into the dialog on open: to initialFocus, else the first control after the header, else Close. For a confirmation, set initialFocus to the least destructive action. On close, focus returns to the control that opened it. The page behind is inert while the dialog is open, so to send focus elsewhere, move it in onClose.
- The Close button is icon-only, named "Close", with its name as a tooltip. Buttons keep their visible text as their name.
- A body taller than the dialog scrolls, and becomes a focusable group named by the title, so it can be scrolled from the keyboard in every browser.
- The surface and text use the raised surface and default text, checked to 4.5:1 in both themes. In forced colors the surface keeps its outline and the backdrop is the system's. The fade is 0ms under reduced motion.
| Key | Action |
|---|---|
| Tab / Shift+Tab | Moves between the dialog's controls. Focus stays inside the dialog while it's open. |
| Escape | Closes the dialog, unless closeOnEsc is false. |
| Enter / Space | Activates the focused button. A DialogButton then closes the dialog. |