Components / overlays
Dialog
variant survives the ban on that prop name, and the reasoning is in CONVENTIONS.md: the public type is modal | sheet | fullscreen, and the further value alert is set internally by AlertDialog and never passed by a caller.
Summary
A decision that must be made before anything else happens.
| APG pattern | Dialog (Modal) |
| Built on | native elements |
| Status | stable |
| Since | 0.1.0 |
variant="sheet" is the same component anchored to an edge, for content that is long rather than urgent. variant="fullscreen" takes the whole viewport, for a task done on a phone in hand — a shopping list read in the aisle. Whatever the variant, the header and footer keep their place and the body is the one part that scrolls.
<Dialog open={open} onClose={close} title="Delete Northlight?"
description="The case study and its 12 images are removed permanently."
footer={<><Button emphasis="transparent">Cancel</Button><Button intent="negative" emphasis="solid">Delete permanently</Button></>}>
<Field id="confirm" label="Type the project name to confirm"><input type="text" /></Field>
</Dialog>
Anatomy
A native <dialog data-se="dialog"> with a header, a body and a footer.
| Part | Element | Required |
|---|---|---|
body | <div> | yes |
close | <button> | yes |
description | <p> | no |
footer | <div> | no |
header | <div> | yes |
title | <p> | yes |
The body is children: nothing inside it touches the dialog's contract.
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
open | boolean | required | Whether the dialog is showing. Drives showModal() and close(), so the focus trap and Escape come from the platform. |
onClose | () => void | required | Called when the dialog closes, however it closed — Escape, the close button, or the backdrop. |
variant | modal · sheet · fullscreen | modal | sheet anchors to the inline end at full height, for long rather than urgent content. fullscreen takes the whole viewport, for a task carried out on a phone in hand. |
title | string | required | The accessible name. A dialog without one announces as a dialog and nothing more. |
description | string | none | What disappears and what breaks. Wired to aria-describedby. |
footer | React.ReactNode | none | Action buttons. Put the confirming action last. |
children | React.ReactNode | none | The body. Nothing in it touches the dialog's contract, which is why this is children and not a named slot. |
id | string | se-dialog | Prefix for the generated title/description ids. |
States
| State | Expressed as | |
|---|---|---|
| Hover | :hover | |
| Open or closed | data-state |
Behavior
The native element does the work. showModal() supplies the focus trap, Escape, the inert background and focus restoration. Nothing here re-implements any of it.
Accessibility
The title is the accessible name, wired by aria-labelledby. A dialog with no name announces as a dialog and nothing more.
Focus returns to whatever opened it. From the platform, and it is the reason this is a real <dialog> rather than a div.
The overlay level is a definition, not a look: it dims the page, traps focus and dismisses on Escape. Anything that does not need all three belongs on the floating level instead.
Keyboard
| Key | Result |
|---|---|
Escape | Closes — from the native <dialog> |
Tab | Cycles inside the dialog and cannot leave it |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-describedby | set at render |
aria-hidden | always true |
aria-label | always Close |
aria-labelledby | set at render |
aria-modal | always true |
Focus
showModal() moves focus in, traps it, and returns it to whatever opened the dialog. All four behaviors are the platform's, which is the reason this is a real <dialog> and not a div.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/browser/card.spec.mjstest/browser/keyboard.spec.mjs
Known gaps
- 27 upstream refs — no datepicker component
Appearance
| Token | Used for |
|---|---|
--se-border | border |
--se-border-width | border |
--se-control-h-sm | block-size, inline-size |
--se-duration-medium | animation |
--se-ease-decelerate | animation |
--se-icon-sm | block-size, inline-size |
--se-layer-overlay | z-index |
--se-leading-body | line-height |
--se-leading-heading | line-height |
--se-radius-lg | border-radius |
--se-radius-md | border-radius |
--se-shadow-overlay | box-shadow |
--se-space-2 | gap |
--se-space-3 | gap, padding |
--se-space-4 | gap |
--se-space-5 | padding, padding-block-end, padding-block-start |
--se-surface | background |
--se-surface-sunk | background |
--se-text | background, color |
--se-text-md | font-size |
--se-text-muted | color |
--se-text-sm | font-size |
--se-tracking-heading | letter-spacing |
--se-weight-semibold | font-weight |
Motion
Entry fades and rises over --se-duration-medium. The entry animation is gated rather than ungated: an ungated one let axe measure a dialog mid-fade and report a contrast failure that existed for 240ms and belonged to nobody.
Leaving is not animated. Leaving should feel decided.
Content
The title asks the question. Delete Northlight? — naming the thing, so a reader who arrived mid-sentence knows which one.
The description says what is lost and whether it comes back. Anyone holding the link will get a 404 is the sentence that changes a decision.
The confirm button repeats the verb. Delete permanently, never OK.
Usage
Where the action is irreversible, ask the reader to type the name. It is the only affordance that reliably stops a reflex click.
Do not use a dialog for something a Popover could carry. A dialog takes the page away; that has to be worth it.
When not to use it. For a confirmation the reader can undo — a Toast with an undo action costs them nothing. For something that must be announced without taking focus — that is an Alert.
Notes
variant survives the ban on that prop name, and the reasoning is in CONVENTIONS.md: the public type is modal | sheet | fullscreen, and the further value alert is set internally by AlertDialog and never passed by a caller.