Diwa Design System

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

KeyAction
EscapeEmits dismiss — close the modal.
TabMove focus to the next focusable element within the modal. Cycles back to the first element when the last is reached.
Shift + TabMove focus to the previous focusable element. Cycles to the last element when the first is reached.
Enter / SpaceActivate the focused button (dismiss, confirm, cancel, etc.).

Screen reader behaviour

The modal panel renders with the following ARIA attributes applied automatically:

PropertyValueNote
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-labelledbyheading element idSet 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

CriterionLevelHow it is met
1.3.1 Info and RelationshipsArole="dialog" + aria-labelledby convey structure to AT.
1.4.3 Contrast (Minimum)AAHeading (#e6e6e7 on #1f1f23): 13:1. Body text (#a1a1aa on #1f1f23): 7.3:1.
2.1.1 KeyboardAAll actions reachable by keyboard. Escape closes the modal.
2.1.2 No Keyboard TrapAFocus is trapped deliberately inside the open dialog (ARIA pattern requirement). Escape always provides an exit.
2.4.3 Focus OrderAFocus moves into the modal on open and returns to the trigger on close.
2.4.7 Focus VisibleAA2px outline on dismiss button via --diwa-border-focus token. 3:1 contrast ratio met.
4.1.2 Name, Role, ValueArole="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: reduce and 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 heading slot; it is used as the dialog's accessible name via aria-labelledby.