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
PropTypeDefaultDescriptionPlatforms
open requiredboolean—

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`.

web, lit, rn, swiftui
heading requiredstring—

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.

web, lit, rn, swiftui
descriptionstring—

One sentence under the title explaining the task or consequence. Becomes the accessible description.

Accessibility: aria-describedby.

web, lit, rn, swiftui
children requiredcontent—

The body — a Form, Text, or controls. Scrolls inside the surface when taller than the viewport; header and footer stay put.

web, lit, rn, swiftui
footercontent—

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.

web, lit, rn, swiftui
hideHeadingbooleanfalse

Visually hide the heading while it remains the accessible name (BottomSheet forwards its own hideHeading here above the breakpoint).

web, lit, rn, swiftui
sizesm | md | lgmd

Surface width on wide viewports. Full-width below the content measure on every size.

web, lit, rn, swiftui
dismissiblebooleantrue

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.

web, lit, rn, swiftui
initialFocusfirst | title | closefirst

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.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onCloseonClose / 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).

web, lit, rn, swiftui
onOpenedonOpened / 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.

web, lit, rn, swiftui

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

scrimsurfacefocusScopeheaderheadingdescriptionbodyfootercloseButton

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).

Dialog keyboard support

4 rows
KeyAction
EscapeRequests close with reason escape (even when not dismissible).
TabMoves to the next focusable element inside the dialog.
TabFrom the last element, wraps to the first.
Shift+TabFrom the first element, wraps to the last.

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.foregroundcolor.overlay.surfaceAA
color.foreground.mutedcolor.overlay.surfaceAA
color.linkcolor.overlay.surfaceAA
color.action.ghost.foregroundcolor.overlay.surfaceAA

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.