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.

Rendered from the package · open on its own

Summary

A decision that must be made before anything else happens.

APG patternDialog (Modal)
Built onnative elements
Statusstable
Since0.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.

PartElementRequired
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

PropertyValuesDefaultWhat it does
openbooleanrequiredWhether the dialog is showing. Drives showModal() and close(), so the focus trap and Escape come from the platform.
onClose() => voidrequiredCalled when the dialog closes, however it closed — Escape, the close button, or the backdrop.
variantmodal · sheet · fullscreenmodalsheet 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.
titlestringrequiredThe accessible name. A dialog without one announces as a dialog and nothing more.
descriptionstringnoneWhat disappears and what breaks. Wired to aria-describedby.
footerReact.ReactNodenoneAction buttons. Put the confirming action last.
childrenReact.ReactNodenoneThe body. Nothing in it touches the dialog's contract, which is why this is children and not a named slot.
idstringse-dialogPrefix for the generated title/description ids.

States

StateExpressed as
Hover:hover
Open or closeddata-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

KeyResult
EscapeCloses — from the native <dialog>
TabCycles inside the dialog and cannot leave it

Roles, states and properties

AttributeSet by
aria-describedbyset at render
aria-hiddenalways true
aria-labelalways Close
aria-labelledbyset at render
aria-modalalways 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.mjs
  • test/browser/keyboard.spec.mjs

Known gaps

  • 27 upstream refs — no datepicker component

Appearance

TokenUsed for
--se-borderborder
--se-border-widthborder
--se-control-h-smblock-size, inline-size
--se-duration-mediumanimation
--se-ease-decelerateanimation
--se-icon-smblock-size, inline-size
--se-layer-overlayz-index
--se-leading-bodyline-height
--se-leading-headingline-height
--se-radius-lgborder-radius
--se-radius-mdborder-radius
--se-shadow-overlaybox-shadow
--se-space-2gap
--se-space-3gap, padding
--se-space-4gap
--se-space-5padding, padding-block-end, padding-block-start
--se-surfacebackground
--se-surface-sunkbackground
--se-textbackground, color
--se-text-mdfont-size
--se-text-mutedcolor
--se-text-smfont-size
--se-tracking-headingletter-spacing
--se-weight-semiboldfont-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.