Skip to main content
BM

Dialog

A modal dialog on the native <dialog> element, for a confirmation, a short form or a message that needs an answer.

View source on GitHub

Delete “Ocean”?

The theme is removed from this browser. This can’t be undone.

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

Rename theme

Full size

Theme preview

A full dialog fills a narrow viewport, for content that needs the room. From 48em it leaves a margin all round, so the page behind still shows.

No backdrop close

Unsaved changes

A click on the dimmed page leaves this dialog open. Escape and Close still close it.

Long body

How themes are kept

Themes you make here are saved in this browser only, under this site's storage.

Clearing your browser's site data removes them, and so does Safari after seven days without a visit.

Exporting a theme downloads a design tokens file, which you can import again later.

Imported files are checked before anything changes: unknown tokens and out-of-range values are listed, and nothing is applied.

Saved themes appear in the site's theme menu next to Light and Dark, and apply to every page.

Deleting the theme you're using switches the site back to Light or Dark, whichever matches it.

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.

Dialog props
PropTypeDefaultDescription
openboolean—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.
titleReactNode—Required. The heading at the top, and the dialog's accessible name.
titleIconIconName—A decorative icon before the title.
titleLevel2 | 3 | 4 | 5 | 62The 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.
closeOnBackdropbooleantrueCloses on a click on the dimmed page. A press that starts inside the dialog doesn't count.
closeOnEscbooleantrueCloses 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.
initialFocusRefObject<HTMLElement | null>—The element focused on open. Default: the first focusable element after the header, else Close.
childrenReactNode—The content under the header: usually a DialogBody, then DialogActions.
DialogActions props
PropTypeDefaultDescription
justify"start" | "end" | "center" | "space-between""end"Where the actions sit along the row. They wrap on narrow screens.
DialogButton props
PropTypeDefaultDescription
result"success" | "failure"—Reported through the dialog's onComplete. Without one, the button only closes the dialog, after its own onClick.

Component tokens

Dialog component tokens
TokenCopyDescriptionAliasesLightDarkPreview
--component-dialog-backgroundDialog surface.color.surface.raised#fffffffrom color.gray.0#18202efrom color.slate.900
--component-dialog-textDialog text.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-dialog-borderSurface 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-widthSurface outline width.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-dialog-radiusSurface corner radius (square when a full dialog fills a narrow viewport).radius.surface4pxfrom radius.44pxfrom radius.4
--component-dialog-shadowSurface shadow: the dialog floats above the page.effect.elevation.overlay0px 4px 12px 0px rgb(0% 0% 0% / 0.12)from effect.shadow.md0px 4px 12px 0px rgb(0% 0% 0% / 0.4)from effect.shadow.md-dark
--component-dialog-paddingPadding around the header, body and actions.space.inset.lg1.5remfrom space.61.5remfrom space.6
--component-dialog-gapSpace between the header, body and actions, and between actions.space.stack.md1remfrom space.41remfrom space.4
--component-dialog-backdropScrim over the page behind a modal dialog.color.fill.scrimrgba(14, 20, 27, 0.4)from color.overlay.gray-950-40rgba(14, 20, 27, 0.6)from color.overlay.gray-950-60
--component-dialog-max-widthWidest default dialog: a readable line length for its text.type.measure.text65chfrom type.line-length.6565chfrom type.line-length.65No preview
--component-dialog-durationFade in and out (0ms under reduced motion).motion.duration.base250msfrom motion.time.250Reduced motion: 0ms250msfrom motion.time.250No preview
--component-dialog-easing-enterEasing while opening.motion.easing.entercubic-bezier(0, 0, 0.2, 1)from motion.curve.outcubic-bezier(0, 0, 0.2, 1)from motion.curve.outNo preview
--component-dialog-easing-exitEasing while closing.motion.easing.exitcubic-bezier(0.4, 0, 1, 1)from motion.curve.incubic-bezier(0.4, 0, 1, 1)from motion.curve.inNo 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.
Dialog keyboard
KeyAction
Tab / Shift+TabMoves between the dialog's controls. Focus stays inside the dialog while it's open.
EscapeCloses the dialog, unless closeOnEsc is false.
Enter / SpaceActivates the focused button. A DialogButton then closes the dialog.