User Interface (UI)
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
- Backdrop — The
::backdropbehind the dialog, which dims the page while it's out of reach. - Panel — The
<dialog>itself, which carries the size and holds every other part. - Title — An
<h2>that says what the dialog is for. It names the dialog. - Close button — A Button, named "Close", that asks to close the dialog. Alert dialogs leave it out.
- Description — Optional. A sentence or two that screen readers read along with the title.
- Body — Optional. The content, like a message or a form, which scrolls when it doesn't fit.
- 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.
<Dialog id="share" size="sm" title="Share project" open={open} onClose={() => setOpen(false)}>
…
</Dialog>
{/* …md, lg */}
| Size | Max. width | Good for |
|---|---|---|
sm | 384px | Confirmations, with a sentence or two and their actions |
md | 512px | Short forms and messages, like renaming a file |
lg | 768px | Longer 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.
<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
Tabreaches them, in that order. Give the main actioncolor="primary", orcolor="error"when it destroys something, and keep to one of them per dialog. - The close button is an icon-only,
ghostButton named "Close", whichcloseLabeltranslates. 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.
| State | Selector | Recommended look |
|---|---|---|
| Closed | :not([open]) | Hidden, by the browser's own styles |
| Open | [open], and :modal once showModal() ran | The panel in the middle of the screen, over the backdrop, which dims the page |
| Opening | @starting-style | A 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:
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>
);
}
.wide {
--dialog--width: 60rem;
}
UI Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
dialog/contrast | Must | Component | The title, the description and the content have a contrast of 4.5:1 against the panel | WCAG 1.4.3 (AA) | axe color-contrast, contrast checker |
dialog/backdrop | Should | Component | A backdrop dims the page behind an open dialog, and a border sets the panel apart where the backdrop barely shows | Open Components | Review |
dialog/fits-screen | Must | Component | It's never wider or taller than the screen, less a margin, down to 320px wide | WCAG 1.4.10 (AA) | Emulation |
dialog/resizable | Must | Component | It's sized in rem, so it grows with its text, and its title wraps rather than getting cut off | Open Components, beyond WCAG 1.4.4, 1.4.12 (AA) | 200% zoom |
dialog/actions-in-view | Should | Component | The title and the actions stay in view, and only the body scrolls, when the content doesn't fit | Open Components | Emulation |
dialog/fitting-size | Should | Usage | Its size fits its content, like sm for a confirmation and lg for a long form | Open Components | Review |
dialog/reduced-motion | Should | Component | It only animates as it opens when prefers-reduced-motion is no-preference | WCAG 2.3.3 (AAA) | Emulation |
dialog/forced-colors | Must | Component | The panel's border stays visible in forced colours mode | Open Components | Emulation |
dialog/more-contrast | Should | Component | The panel's border is drawn at full strength when prefers-contrast: more is set | Open Components | Emulation |
dialog/mirrors-in-rtl | Should | Component | It uses logical properties, so its close button and actions mirror in right-to-left languages | Open Components | Review |