Dialog

User Interface (UI)

How a dialog looks, from its parts and how wide it gets to what goes in it, how it shows that it's open and its tokens.

Let's start with how a dialog looks: its parts, how wide it gets, what goes in it, how it shows that it's open and how to tune it.

Anatomy

  1. Backdrop — The ::backdrop behind the dialog, which dims the page while it's out of reach.
  2. Panel — The <dialog> itself, which carries the size and holds every other part.
  3. Title — An <h2> that says what the dialog is for. It names the dialog.
  4. Close button — A Button, named "Close", that asks to close the dialog. Alert dialogs leave it out.
  5. Description — Optional. A sentence or two that screen readers read along with the title.
  6. Body — Optional. The content, like a message or a form, which scrolls when it doesn't fit.
  7. Actions — Optional. The Buttons that answer the dialog, like Cancel and Save.

Every dialog has a panel and a title. The title, the close button and the description sit in a header at the top, and the actions in a row at the bottom, so people always know what they're answering and how. The parts always render in the same order, which you can see in the DOM contract.

Sizes

The size prop sets how wide the dialog gets, at most, and md is the default. We recommend picking the smallest size that fits the content, since a short question in a wide panel leaves people reading lines that run across the screen.

A sm dialog

It gets as wide as its size allows, and never wider than the screen, less a margin.

A md dialog

It gets as wide as its size allows, and never wider than the screen, less a margin.

A lg dialog

It gets as wide as its size allows, and never wider than the screen, less a margin.

<Dialog id="share" size="sm" title="Share project" open={open} onClose={() => setOpen(false)}>
  …
</Dialog>
{/* …md, lg */}
SizeMax. widthGood for
sm384pxConfirmations, with a sentence or two and their actions
md512pxShort forms and messages, like renaming a file
lg768pxLonger forms and content, like settings or a list to pick from

Sizes are set in rem, and the table shows the values we recommend at the default text size of 16px. Whatever its size, the dialog is never wider or taller than the screen, less 16px on each side, so on a phone 320px wide it fills the screen, with the page's edge still showing around it (WCAG 1.4.10). When its content is taller than that, the body scrolls, while the title and the actions stay in view.

Title, content and actions

A dialog's content goes in three places: a title and an optional description at the top, the body in the middle and the actions at the bottom.

Edit profile

Your name and email address, as other people see them.

<Dialog
  id="edit-profile"
  title="Edit profile"
  description="Your name and email address, as other people see them."
  open={open}
  onClose={() => setOpen(false)}
  actions={
    <>
      <Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
      <Button color="primary" onClick={save}>Save changes</Button>
    </>
  }
>
  <label htmlFor="name">Full name</label>
  <Input id="name" autoComplete="name" />
  <label htmlFor="email">Email address</label>
  <Input id="email" type="email" autoComplete="email" />
</Dialog>
  • The title says what the dialog is for, in a few words, like "Edit profile" or "Delete project?". It's also the dialog's name, so it's what screen readers announce as focus moves in, and what voice control users and agents find it by.
  • The description is for a sentence or two that people need before they answer, like what a delete will take with it. Screen readers read it right after the title. Anything longer, like a form, a list or a table, goes in the body instead, which isn't part of the description, since screen readers would read it all out in one go, without its structure.
  • The actions are Buttons, named by what they do. We recommend putting the one that leaves things as they are first, like Cancel, and the main one last, at the end of the row, so people read them, and Tab reaches them, in that order. Give the main action color="primary", or color="error" when it destroys something, and keep to one of them per dialog.
  • The close button is an icon-only, ghost Button named "Close", which closeLabel translates. It sits at the top, beside the title, where people look for it.

States

The Dialog has two states of its own, open and closed, and the page around it has one too, since it's inert while the dialog is open.

StateSelectorRecommended look
Closed:not([open])Hidden, by the browser's own styles
Open[open], and :modal once showModal() ranThe panel in the middle of the screen, over the backdrop, which dims the page
Opening@starting-styleA fade and a grow from 96% over 150ms, only when people haven't asked for less motion

Hover, focus and disabled belong to the controls inside a dialog, like its Buttons and Inputs, which show them the same way they do everywhere else. You'll find how each state behaves under Every state.

Tokens

The panel looks just like a Card, with the same padding, spacing, corners, border and type, so the two read as one family. Only its background differs, since it has to cover the page: it's Canvas, the background browsers give a native dialog, which follows color-scheme, so it fits your light and dark themes as long as they set it.

Its text reads one theme token, --color--neutral-text, like the Card's, and the backdrop is black at 50%, which dims a light page and a dark one alike. The Buttons inside read their own tokens, so copy tokens.css from the Button into your theme.

With our reference tokens, the title and the content come to 14.9:1 on white, and the description, at 70% of the text colour, to 5.6:1. The panel has a 1px border at 20% of the text colour, which also sets it apart on a dark page, where the backdrop barely shows. If you change the tokens or the background, make sure to check the same pairs.

If you need to tune a dialog, you can set its own variables, such as --dialog--width, --dialog--padding or --dialog--backdrop--background-color, from a class. The Dialog's styles sit in the components cascade layer, so your class wins without any specificity tricks:

Settings.tsx
import styles from "./Settings.module.css";

export function Settings({ open, onClose }: { open: boolean; onClose: () => void }) {
  return (
    <Dialog id="settings" title="Settings" className={styles.wide} open={open} onClose={onClose}>
      …
    </Dialog>
  );
}
Settings.module.css
.wide {
  --dialog--width: 60rem;
}

UI Checklist

RuleLevelScopeRequirementBasisCheck
dialog/contrastMustComponentThe title, the description and the content have a contrast of 4.5:1 against the panelWCAG 1.4.3 (AA)axe color-contrast, contrast checker
dialog/backdropShouldComponentA backdrop dims the page behind an open dialog, and a border sets the panel apart where the backdrop barely showsOpen ComponentsReview
dialog/fits-screenMustComponentIt's never wider or taller than the screen, less a margin, down to 320px wideWCAG 1.4.10 (AA)Emulation
dialog/resizableMustComponentIt's sized in rem, so it grows with its text, and its title wraps rather than getting cut offOpen Components, beyond WCAG 1.4.4, 1.4.12 (AA)200% zoom
dialog/actions-in-viewShouldComponentThe title and the actions stay in view, and only the body scrolls, when the content doesn't fitOpen ComponentsEmulation
dialog/fitting-sizeShouldUsageIts size fits its content, like sm for a confirmation and lg for a long formOpen ComponentsReview
dialog/reduced-motionShouldComponentIt only animates as it opens when prefers-reduced-motion is no-preferenceWCAG 2.3.3 (AAA)Emulation
dialog/forced-colorsMustComponentThe panel's border stays visible in forced colours modeOpen ComponentsEmulation
dialog/more-contrastShouldComponentThe panel's border is drawn at full strength when prefers-contrast: more is setOpen ComponentsEmulation
dialog/mirrors-in-rtlShouldComponentIt uses logical properties, so its close button and actions mirror in right-to-left languagesOpen ComponentsReview
Copyright © 2026