Dialog

A modal on the native element, in four widths. The page behind goes inert, Esc asks the caller to close, and a click beside the dialog does nothing.

Try it

Cancel this appointment?

The client is told by email, and the slot opens to others.

Focus lands on Keep it, so Enter keeps the appointment; only the red button cancels it.

Choosing a dialog

A dialog interrupts. It is for a decision that cannot wait — confirm, discard, choose — or for a short form that belongs to the page beneath it and would lose its place as a page of its own. It is not for content the reader browses, which is a page, and not for news, which is a status message: nothing that can be read later should stop the reader now.

The buttons answer the title. Under Cancel this appointment? they are Cancel appointment and Keep it, so the dialog can be answered without reading its body — never Yes and No, and never a bare Cancel that could mean either. The destructive button takes the danger tone and is the only filled one; the safe button is outline, and it is where focus starts. In a form the confirm names what the form does: Add staff member, not Save, so the button reads the same as the title that opened it.

xs holds a question and two answers. sm holds a short form. md holds a form with two columns, and lg a picker or a table the reader chooses from. If the body needs to scroll on a laptop, it is a page.

Anatomy and sizes

Three bands. The header holds the back button when there is a previous step, the title, and the close button, at 24px of padding; the body takes 40px; the footer holds the actions in reading order, the safe one first, and it is not drawn without them. Hairlines separate the bands. The corner is 16px, and the backdrop is the scrim measured in the margin.

Reschedule appointment

Move Thursday's 10:30 appointment to the next free slot with the same clinician.

Reschedule appointment

Move Thursday's 10:30 appointment to the next free slot with the same clinician.

Reschedule appointment

Move Thursday's 10:30 appointment to the next free slot with the same clinician.

Reschedule appointment

Move Thursday's 10:30 appointment to the next free slot with the same clinician.

On a screen narrower than the dialog it takes the width less 16px each side, and on xs the actions stack, the first on top.

A form

Add staff member

The first field takes focus through initialFocus. Not autoFocus: React's client renderer does not write the attribute that showModal() looks for, so it would work on a server-rendered page and not on this one. The confirm button submits the form by its form attribute, from outside it.

More than one step

Choose a clinic

The colleague joins the clinic you choose here, with its opening hours and rooms.

The back button appears only with onBack. A single question has no previous step to go back to.

Decisions

The backdrop is the drawn wash. The drawing's overlay is stone/200 at 95%: the page behind all but disappears. The dialog is separated from it by its shadow — on colour alone the two are 1.35:1. Dark was never drawn. Mirroring the wash onto the overlay colour measures 1.01:1, and the dialog would vanish, so dark uses the system's darkest ink at the same 95%: 1.19:1, with a hairline border on the dialog.

The title is primary. The drawing has it in text/tertiary, the level for helper text and timestamps. Tertiary passes AA at 5.38:1; the title is the most important text in the dialog, and primary reads 16.28:1.

A click on the backdrop does not close. Esc, the close button and the caller's actions do. A stray click beside a half-filled form must not throw the form away.

Accessibility

The dialog is named by its title. showModal() puts it in the top layer and makes the rest of the page inert, so neither Tab nor a screen reader reaches what is behind it, and closing returns focus to the element that opened it.

Esc does not close the dialog by itself: it calls onClose, and the caller sets open to false — or does not, to keep unsaved input. That refusal holds for one Esc. Chrome closes a dialog on a second Esc without other interaction, and onClose reports that close too, so the caller's state never says open while the dialog is shut.

When the back button disappears — the first step of a flow has none — focus would fall to the page behind the dialog. It moves to the close button instead.

Checked by hand in Chrome on 11 September 2026, light and dark: the dialog is modal and in the top layer; Tab cycles inside it; the page does not scroll behind it; the close button closes it and focus returns to the button that opened it; a click on the backdrop leaves it open; the backdrop is the mist in light and the ink in dark, with the border in dark; initialFocus lands on the first field; after Back, focus is on the close button; and at 375px wide the dialog fits the screen. Esc was not checked: the automated browser used does not turn a synthetic Escape into a close request, not even for a plain <dialog>, so that check waits for a keyboard. Safari and Firefox have not been checked.

Props

Dialog props
PropTypeDefault
openboolean
onClose() => void
titlestring
size'xs' | 'sm' | 'md' | 'lg''md'
onBack() => void
actionsReactNode
initialFocusRefObject<HTMLElement | null>