← Todos los specs UI

Contrato del componente

dialog

Elemento <dialog> nativo envuelto como custom element del design system — showModal() entrega la máquina de estado modal (focus trap, Escape para cerrar, background inerte, return-focus) vía la plataforma del navegador, sin ARIA scratch. Props: open, label|labelledBy, describedBy. Eventos: dsOpen, dsClose.

Versión
v1.0.0

Props

NombreTipoValoresPredeterminado
openbooleanfalse
labelstring
labelledBystring
describedBystring

Eventos

NombreCarga útilCondición
dsOpenvoid
dsClosevoidfires on every native close (Escape, form method=dialog, programmatic close(), or open prop transition to false)

Slot

Nombre
default
Propósito
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

Accesibilidad

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"]
JSON validado original
{
  "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"
    ]
  }
}