The library targets WCAG 2.2 AA. Per-component a11y notes (roles, keyboard, screen-reader behavior) live on
each component's docs page and in the cross-system reference.
This page captures the overall approach.
Native first
<dialog> for modals — provides focus containment, Escape-to-close behavior, and role="dialog".
Popover API for menus, popovers, tooltips — provides top-layer rendering. Menus and popovers use
popover="auto", so the platform light-dismisses them and Escape reaches them through the close-watcher
stack. A tooltip uses popover="manual" instead — showing an auto popover closes every other
one, so hovering a control would close the menu containing it — and is dismissed by the trigger losing hover or focus,
or by the shared Escape listener that satisfies the Dismissable half of WCAG 2.2 SC 1.4.13.
CSS Anchor Positioning for placing popovers relative to their triggers, with a JS fallback where the
engine lacks it (see Browser support).
<output> with role="status" or role="alert" for feedback (see Toast).
Native form controls (<input>, <textarea>, <select>) wherever they suffice.
When native falls short, custom elements follow WAI-ARIA APG patterns. Component pages cite the specific APG pattern they implement.
Keyboard
Every interactive element is reachable by Tab.
Composite widgets (toolbar, menu, tabs) use roving tabindex so the whole widget is a single tab stop; arrow keys navigate within.
Escape closes modal / popover / menu and returns focus to the trigger.
Check how the component handles Escape before adding an ancestor handler.
Cancelling the default action and stopping propagation have separate effects:
Cancelled, with propagation allowed. Ancestor listeners run after
the overlay closes. Check e.defaultPrevented; checking whether an overlay
is still open will miss the one that just handled Escape.
Cancelled, with propagation stopped. Bubble-phase ancestor handlers
do not run. Capture-phase handlers run before cancellation and cannot observe it yet.
Cancellation also prevents the browser’s close watcher from closing another overlay.
Neither cancelled nor stopped. The component leaves Escape for an
enclosing widget, or respects an event already handled by an inner widget. Its response
may depend on its current state.
Propagation stopped without cancellation. No component here uses
this combination. It would block ancestor listeners while leaving the browser’s close
request active, potentially closing another overlay after the first has hidden.
Before adding an Escape handler to an element that can contain an overlay, read the overlay's documentation:
Menu,
Popover,
Combobox,
Notifications and
Select.
Enter / Space activate buttons and links.
Screen reader labels
Icon-only buttons must have an accessible name — via aria-label or a <span class="visually-hidden"> inside. title alone is unreliable.
Form fields always pair with a <label for="…"> or a <legend> inside a <fieldset>. aria-label is a last resort.
Badge counts that convey information (e.g. "3 unread") need context — aria-label on the parent or a visually-hidden phrase.
Multiple landmarks of the same type (<nav>, <aside>) need distinct aria-labels.
Focus management
Modals move focus into the dialog on open and return it to the trigger on close (handled by <ui-dialog>).
Alert dialogs (<ui-dialog alertdialog>) put autofocus on the least
destructive action, so initial focus lands on Cancel rather than Delete — see
Dialog.
Toasts do not steal focus — they announce via role="status" / role="alert". The persistent <ui-statusbar> projection uses the same polite role="status" for its inline live region.
Tooltips never contain interactive content.
Motion
Wrap transitions and keyframe animations in @media (--motionOK) or @media (prefers-reduced-motion: no-preference).
Keep final-state rules outside the motion query — a component still reflects state under reduce-motion; only the animation is removed.
Auto-rotating content (e.g. carousel) must be pausable (WCAG 2.2.2).
Toast auto-dismiss must be pausable on hover (same WCAG rule).
Don't rely on color alone to convey state. Status-success uses green plus a checkmark icon or explicit label; status-error uses red plus the .status-error animation.
3-state theming (System / Light / Dark) always respects the user's choice. See Color § 3-state theming.
Avoid hand-authored RGB hex + opacity for translucency — color-mix(in oklch, …) composes more reliably across themes.
Testing
Automated:npm run test:wpt (Playwright, Chromium + Firefox) covers keyboard
interaction and role assertions per component in a real engine; npm test (happy-dom) asserts the roles,
ARIA attributes and live-region wiring that don't need one.
Manual: tab through new UIs with keyboard alone; test with at least one screen reader (VoiceOver on macOS, NVDA on Windows).
Use browser devtools' accessibility tree view to verify role, name, and state.