Dialog
A modal window over the page for a task that must be finished or abandoned before returning — focus trapped, background inert, closed by Escape, focus restored to the opener.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Dialog } from '@design-schema/react'; Examples
import { useEffect, useState, type ReactElement } from 'react';
import { Dialog, type DialogProps, Button, Input } from '@design-schema/react';
function DialogHarness({ open: initialOpen, onClose, ...rest }: DialogProps): ReactElement {
const [open, setOpen] = useState(initialOpen);
useEffect(() => setOpen(initialOpen), [initialOpen]);
return (
<>
<Button label={rest.heading} onClick={() => setOpen(true)} />
<Dialog
{...rest}
open={open}
onClose={(reason) => {
onClose?.(reason);
setOpen(false);
}}
/>
</>
);
}
const renameFooter = (
<>
<Button label="Rename" variant="primary" />
<Button label="Cancel" variant="secondary" />
</>
);
<DialogHarness open heading="Rename project" footer={renameFooter}><Input label="Project name" name="projectName" defaultValue="Q3 roadmap" /></DialogHarness>Props
9 rows| Prop | Type | Default | Description |
|---|---|---|---|
| open required | boolean | — | Controlled only — there is no uncontrolled mode and no initial-state prop; the consumer owns `open` and the dialog never closes itself, it requests changes through `onClose`. |
| heading required | string | — | The dialog's title, rendered as a level-2 Heading and used as the accessible name. Says what the task is ("Rename project"). Accessibility: aria-labelledby the heading; native accessibilityLabel on the Modal content. |
| description | string | — | One sentence under the title explaining the task or consequence. Becomes the accessible description. Accessibility: aria-describedby. |
| children required | content | — | The body — a Form, Text, or controls. Scrolls inside the surface when taller than the viewport; header and footer stay put. |
| footer | content | — | The action row. Primary action first, then one secondary; follows Form's action-order rule. A dialog with no footer must be dismissable from its body. |
| hideHeading | boolean | false | Visually hide the heading while it remains the accessible name (BottomSheet forwards its own hideHeading here above the breakpoint). |
| size | sm | md | lg | md | Surface width on wide viewports. Full-width below the content measure on every size. |
| dismissible | boolean | true | Escape, the close button and a scrim click all request close. Set false for a dialog that must be answered (then provide the answers in the footer): the close button is not rendered and the scrim does nothing; Escape still fires `onClose` with reason `escape` so the consumer can decide. |
| initialFocus | first | title | close | first | Where focus lands on open: the first focusable control (default), the title (for long or reading dialogs), or the close button. `first` looks in the body, then the footer, then the close button, then the heading (tabindex -1); `close` with no close button rendered (not dismissible) takes the same order without the close button. Accessibility: Focus must move into the dialog on open and never rest on the scrim or the page behind. |
Events
2 rows| Event | Type | Description |
|---|---|---|
| onClose | onClose / close | Fired when the user requests to close, with a reason: `escape`, `close-button`, `scrim`, or `action`. The consumer sets `open` to false (or not). |
| onOpened | onOpened / opened | Fired after the open transition ends and focus has moved in. When there is no transition to wait for (reduced motion, or a zero computed duration), it fires on the next frame (requestAnimationFrame on every platform) after focus moves in. It does not fire when `open` becomes false before the enter transition finishes — including on that reduced-motion path, where the pending frame is cancelled. Use to start work that needs the dialog visible. |
Accessibility
Decisions this component leaves to you
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Required props that carry accessibility meaning: heading. Each is described in the props table above.
Implicit ARIA role: dialog.
Anatomy
What generation guarantees
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Focus trap — While it is open, Tab cycles inside it; focus cannot reach the page behind.
Focus restore — On close, focus returns to whatever opened it.
Escape dismisses — Escape closes it from anywhere inside.
Inert background — While it is open, the rest of the page is inert to the pointer and to assistive technology.
Scroll lock — While it is open, the page behind it does not scroll.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.
Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.