Modal
An overlay dialog that focuses the user's attention on a single task or piece of information. The page behind is blocked from interaction until the modal is dismissed.
Keyboard interaction
| Key | Action |
|---|---|
| Escape | Emits dismiss — close the modal. |
| Tab | Move focus to the next focusable element within the modal. Cycles back to the first element when the last is reached. |
| Shift + Tab | Move focus to the previous focusable element. Cycles to the last element when the first is reached. |
| Enter / Space | Activate the focused button (dismiss, confirm, cancel, etc.). |
Screen reader behaviour
The modal panel renders with the following ARIA attributes applied automatically:
| Property | Value | Note |
|---|---|---|
role | "dialog" | Declares this element as a dialog. Screen readers announce entry and exit. |
aria-modal | "true" | Tells assistive technologies that content outside is inert while the dialog is open. |
aria-labelledby | heading element id | Set automatically when the heading prop is provided. Links the dialog name to the visible heading text. |
aria-label | "Dialog" | Applied only when no heading prop is set, as a fallback accessible name. |
aria-hidden | "true" (when closed) | Hides the panel from assistive technologies when open=false. Prevents AT from reading hidden modal content. |
Focus management
On open: Focus moves to the modal panel (tabIndex={-1}). The user can then Tab to the first interactive element inside.
Focus trap: Tab and Shift+Tab cycle focus within the modal's shadow root. Slotted light-DOM content (footer buttons, form fields) participates in the browser's natural tab order. The dialog's aria-modal="true" prevents virtual cursor navigation to background content in most screen readers.
On close: Focus is returned to the element that triggered the modal — typically the button that set open=true. This is handled automatically by the component.
WCAG 2.2 compliance
| Criterion | Level | How it is met |
|---|---|---|
| 1.3.1 Info and Relationships | A | role="dialog" + aria-labelledby convey structure to AT. |
| 1.4.3 Contrast (Minimum) | AA | Heading (#e6e6e7 on #1f1f23): 13:1. Body text (#a1a1aa on #1f1f23): 7.3:1. |
| 2.1.1 Keyboard | A | All actions reachable by keyboard. Escape closes the modal. |
| 2.1.2 No Keyboard Trap | A | Focus is trapped deliberately inside the open dialog (ARIA pattern requirement). Escape always provides an exit. |
| 2.4.3 Focus Order | A | Focus moves into the modal on open and returns to the trigger on close. |
| 2.4.7 Focus Visible | AA | 2px outline on dismiss button via --diwa-border-focus token. 3:1 contrast ratio met. |
| 4.1.2 Name, Role, Value | A | role="dialog", aria-modal="true", dismiss button has aria-label="Close dialog". |
V1 limitations
Slotted content focus trap: The Tab-key trap operates within the Shadow DOM. Slotted children (light DOM) rely on aria-modal="true" to prevent screen readers from escaping, as a full cross-boundary trap is not yet implemented.
Scroll lock on nested scroll containers: The component locks document.body.overflow which may conflict with custom root scroll containers. Set overflow on your layout root manually if needed.
Best practices
- Reduced motion — all CSS transitions inside the shadow DOM respond to
prefers-reduced-motion: reduceand are suppressed automatically. - Focus management — focus moves into the modal on open and returns to the trigger on close. Do not place the trigger inside the modal content — this breaks the return-focus contract.
- Heading slot — always provide a heading in the
headingslot; it is used as the dialog's accessible name viaaria-labelledby.