Accessibility

Checked by the build, not asserted in a paragraph

Every component is audited against WCAG 2.1 Level AA on every change, and a violation fails the test suite. The audit runs twice: once per story, where a component stands alone, and once over the served site in both themes, where the page's landmarks and heading order exist for the first time. The keyboard and focus rules below are assertions, not guidance.

Coverage

Read from the component source rather than claimed.

133Components audited, all of them
954Stories audited on every change
111Components declaring ARIA roles and attributes
88Components carrying an accessible name in source

What the build enforces

Four gates, all in the same run as everything else. None of them is a separate ritual somebody has to remember.

The four accessibility gates and when each one runs
GateWhat it provesWhen
Component audit
axe runs against every story in headless Chromium. Every variant of every component is audited, not a sample.
Story tests, on every change
Page audit
axe runs again over the served build in both themes, on the WCAG 2.1 A and AA rule set, where landmark structure and heading order are visible for the first time. A component alone cannot show either.
After the website build, in CI and local verify
Theme contrast
Every shipped theme has its action colour and text resolved through the var() chain and held to the AA threshold.
The validate-registry chain
Behaviour assertions
Focus movement, Escape handling and overlay stacking, asserted as story play functions rather than described in a doc.
Story tests, on every change

Themes start from an AA baseline

The shipped themes are examples, and the expectation is that you build your own. What travels is the baseline they are built to: an action colour and the text on it are resolved through the var() chain and held to the AA threshold in light and dark. Every shipped theme clears it, so whichever one you start from is a passing starting point.

What every component inherits

The interactive components are built on this repo's own behaviour layer rather than a third-party primitives library, so the keyboard and focus rules are written once and shared. 5 modules carry them.

The shared behaviour layer and what it guarantees
BehaviourWhat a consumer gets
Focus scope
Tab and Shift+Tab cycle inside an open panel and nowhere else, focus returns to the element that opened it, and the background is marked inert while it is open.
Layer stack
Escape and an outside click reach only the topmost overlay, so a dialog opened over a dialog closes one at a time instead of both at once.
Counted scroll lock
The page unlocks when the last holder releases it, so two overlays never unlock the body under each other. Published as ./behaviors/useScrollLock so host chrome joins the same lock rather than fights it.
Shared focusable query
One definition of what counts as focusable, used by the overlays and by roving arrow-key navigation, so two components never disagree about what Tab should reach.
Reduced-motion guard
A reduced-motion preference collapses every duration token at the token layer, so a component honours the preference by using the scale rather than by remembering to check.

The overlay keyboard contract

Alert dialog, App sidebar, Command palette, Dialog, Drawer and Lightbox share one contract. Story assertions hold them to it, so a regression in any of these rows fails the suite.

Keyboard and focus behaviour shared by the modal overlays
KeyBehaviour
On open
Focus moves into the panel. In AlertDialog it lands on the cancel button, so the safe action is the one under the first keypress and the destructive one is never a stray Enter away.
Tab / Shift+Tab
Moves through the focusable elements inside the panel, and cannot leave it while the overlay is open.
Escape
Dismisses the topmost overlay only. In AlertDialog it cancels, never confirms.
Arrow keys
Move through the list in CommandPalette, and between images in Lightbox.
On close
Focus returns to the trigger, the body scroll lock releases its hold, and the background stops being inert.
Try it with the keyboard

Open the dialog, then press Tab a few times: focus cycles through the three controls and never reaches the page behind. Press Escape and focus lands back on the button you opened it with.