Component contract
dialog
Native <dialog> element wrapped as a design-system custom element — showModal() delivers the modal state machine (focus trap, Escape to close, inert background, return-focus) via the browser platform, not scratch-built ARIA. Props: open, label|labelledBy, describedBy. Events: dsOpen, dsClose.
- Version
- v1.0.0
Props
| Name | Type | Values | Default |
|---|---|---|---|
| open | boolean | — | false |
| label | string | — | — |
| labelledBy | string | — | — |
| describedBy | string | — | — |
Events
| Name | Payload | Condition |
|---|---|---|
| dsOpen | void | — |
| dsClose | void | fires on every native close (Escape, form method=dialog, programmatic close(), or open prop transition to false) |
Slot
- Name
- default
- Purpose
- dialog content — headings, body copy, form fields, and action buttons. Focus lands on the first `autofocus` element if present, or the first focusable descendant otherwise.
Tokens
- spacing
- xl
- radius
- md
Accessibility
- native_element
- dialog (opened via showModal())
- apg_pattern
- https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
- role
- implicit `dialog` (native); `aria-modal="true"` implicit when opened via showModal()
- labeling
- Choose ONE: `label` (string → aria-label) for short titles, OR `labelledBy` (id of a light-DOM heading inside the slot → aria-labelledby via flattened-tree resolution) for dialogs with a visible title. Omitting both fails axe `dialog-name`.
- keyboard
- ["Tab / Shift+Tab: focus trap (native, cycles within slotted descendants)","Escape: closes and returns focus (native, fires `close` event which we re-emit as dsClose)","Enter / Space / arrow keys / printable chars: forwarded to focused descendant","Home / End: no dialog-level behavior; forwarded to descendants"]
- focus_lifecycle
- {"opening":"browser focuses the first `autofocus` element inside the slot, or the first focusable descendant if none","while_open":"Tab / Shift+Tab cycle within the flattened tree of the <dialog>; focus cannot escape into the inert background","closing":"focus returns to the element that had it immediately before showModal() was called; native, not implemented by us"}
- motion
- no component-owned animation; consumers can style `dialog[open]` and `::backdrop` with their own animation, gated on `@media (prefers-reduced-motion: no-preference)`
- notes
- This is the deliberate answer to the a11y spec §D 'do not scratch-build state-machine components' rule — wrapping the native <dialog> delegates the state machine to the browser platform, which is verified to trap focus and restore correctly. Escape is native and always closes; consumers who need to guard destructive flows must call `event.preventDefault()` on the dialog's keydown before it reaches the browser. `spec.state_machine` is intentionally omitted from this contract: the state machine is owned by the browser (the native <dialog> element via showModal()/close()), not synthesized by this component — so declaring it in the spec would misattribute ownership and trip the L1-with-state-machine doctor warning.
- pending_manual_verification
- ["VoiceOver on macOS: announces accessible name + 'dialog' on open, initial focus lands on autofocus target, Escape closes and focus returns to trigger","Chromium high-contrast / forced-colors: surface reads as bordered container, ::backdrop dim is visible"]
Raw validated JSON
{
"name": "dialog",
"version": "1.0.0",
"props": {
"open": {
"type": "boolean",
"default": false
},
"label": {
"type": "string"
},
"labelledBy": {
"type": "string"
},
"describedBy": {
"type": "string"
}
},
"events": {
"dsOpen": {
"payload": "void"
},
"dsClose": {
"payload": "void",
"condition": "fires on every native close (Escape, form method=dialog, programmatic close(), or open prop transition to false)"
}
},
"slot": {
"name": "default",
"purpose": "dialog content — headings, body copy, form fields, and action buttons. Focus lands on the first `autofocus` element if present, or the first focusable descendant otherwise."
},
"tokens_used": {
"spacing": [
"xl"
],
"radius": [
"md"
]
},
"a11y": {
"native_element": "dialog (opened via showModal())",
"apg_pattern": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/",
"role": "implicit `dialog` (native); `aria-modal=\"true\"` implicit when opened via showModal()",
"labeling": "Choose ONE: `label` (string → aria-label) for short titles, OR `labelledBy` (id of a light-DOM heading inside the slot → aria-labelledby via flattened-tree resolution) for dialogs with a visible title. Omitting both fails axe `dialog-name`.",
"keyboard": [
"Tab / Shift+Tab: focus trap (native, cycles within slotted descendants)",
"Escape: closes and returns focus (native, fires `close` event which we re-emit as dsClose)",
"Enter / Space / arrow keys / printable chars: forwarded to focused descendant",
"Home / End: no dialog-level behavior; forwarded to descendants"
],
"focus_lifecycle": {
"opening": "browser focuses the first `autofocus` element inside the slot, or the first focusable descendant if none",
"while_open": "Tab / Shift+Tab cycle within the flattened tree of the <dialog>; focus cannot escape into the inert background",
"closing": "focus returns to the element that had it immediately before showModal() was called; native, not implemented by us"
},
"motion": "no component-owned animation; consumers can style `dialog[open]` and `::backdrop` with their own animation, gated on `@media (prefers-reduced-motion: no-preference)`",
"notes": "This is the deliberate answer to the a11y spec §D 'do not scratch-build state-machine components' rule — wrapping the native <dialog> delegates the state machine to the browser platform, which is verified to trap focus and restore correctly. Escape is native and always closes; consumers who need to guard destructive flows must call `event.preventDefault()` on the dialog's keydown before it reaches the browser. `spec.state_machine` is intentionally omitted from this contract: the state machine is owned by the browser (the native <dialog> element via showModal()/close()), not synthesized by this component — so declaring it in the spec would misattribute ownership and trip the L1-with-state-machine doctor warning.",
"pending_manual_verification": [
"VoiceOver on macOS: announces accessible name + 'dialog' on open, initial focus lands on autofocus target, Escape closes and focus returns to trigger",
"Chromium high-contrast / forced-colors: surface reads as bordered container, ::backdrop dim is visible"
]
}
}