Theme: Smoke
Background: Field
Theme: Smoke
Background: Field
System

Accessibility

Built for whoever reaches it, however they reach it

Some people tab through a page. Others hear it read aloud, zoom it to twice its size, or switch it to dark at midnight. A component that only works with a mouse and perfect eyesight shuts all of them out, and every product built from it inherits the same wall. So here accessibility is part of what a component is, settled once in the library instead of remembered screen by screen.

The build holds the line. Every component is audited against WCAG 2.1 Level AA on every change, alone and again inside the pages it sits in, and a failure stops the change. The rest of this page is what that covers, and what still needs a person.

Coverage

100%Of components audited against WCAG 2.1 AA
1,520Stories rendered and checked on every change
4.5:1Contrast every action holds, in every theme
25Overlays that close one layer at a time

What the build enforces

Every gate runs in the same run as everything else. None of them is a separate ritual somebody has to remember.

The 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
Forced colors
High contrast modes swap every colour for the user's own and flatten every fill, so the token layer re-points the roles that carry a state (a checked box, the selected segment, the primary button, a count) at system colours, the one fill the mode keeps. The table bench is rendered in that mode, and the check fails on a state with no fill of its own, an open panel with no edge or a focus ring that vanished.
After the website build, in CI and local verify
Theme contrast
Every action fill in every shipped theme, at rest, on hover and when pressed, is resolved through the var() chain and held against the label or icon drawn on it: 4.5:1 for text, 3:1 for icons, in light and dark.
The validate-registry chain
Behaviour assertions
Focus movement, Escape handling and overlay stacking, including a popover, a menu and a tooltip open inside a modal, asserted as story play functions rather than described in a doc.
Story tests, on every change
Names and attributes
Every name a component announces, and every word it writes on screen, is a prop with an English default, so a product in another language can translate what a screen reader reads. The build fails on English written into the markup itself. Every component also passes a consumer's own aria-* attributes and a ref through to its element, proven by rendering each one.
The validate-registry chain, and story tests

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: every action fill, at rest, on hover and when pressed, is resolved through the var() chain and held against the label or icon drawn on it, AA for text and 3:1 for icons, 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. 11 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, and the background is marked inert while it is open, except a live region such as the toast stack, which keeps announcing. On close, focus returns to the element that opened it, or to the nearest visible control when that element was hidden in the meantime.
Layer stack
Escape reaches only the topmost overlay, modal or not, so a dialog opened over a dialog, or a menu open inside one, closes one at a time. An outside press closes the popovers and menus above it and stops at the first layer that contains it, so a press on a dialog's backdrop with a menu open closes only the menu.
Keyboard shortcuts
Every key binding, the library's and your app's, goes through one listener. A shortcut stays quiet while someone types in a field, never takes Escape from an open overlay, and a page shortcut waits while a dialog or menu holds focus. Published as ./behaviors/shortcuts, with a list of the live bindings for a help dialog.
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.
Anchored position
Every menu, listbox, picker, popover and tooltip is placed against its control so it shows whole: it flips to the other side when one has no room, stays inside the screen, and follows its control on scroll, so content inside a scrolling table or a narrow sidebar is never cut off where a keyboard or magnifier user cannot reach 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, Attachment viewer, Bento tile, Command palette, Dialog, Drawer and Lightbox share one contract, because they share the code behind it: the dialog stories assert every row below, so a regression in the shared layer fails the suite for all of them. The popovers, menus, pickers and tooltips do not hold focus, but they dismiss through the same stack, so Escape and an outside press reach them first: 25 components in all answer to it.

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, including a popover, menu or visible tooltip open inside the dialog, which closes before the dialog does. In AlertDialog it cancels, never confirms.
Arrow keys
Move through the list in CommandPalette, and between files in Lightbox and AttachmentViewer, while focus is inside the open panel.
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 dialog’s controls and never reaches the page behind. Press Escape and focus lands back on the button you opened it with.

What still needs a person

axe finds roughly a third of WCAG failures. These are the ones that need someone reading the page.

Accessibility checks the build leaves to human review
WhatWhy the build cannot judge it
Tab order on a page
The stories prove focus is held inside a dialog and handed back after. Whether Tab moves through a whole page in an order that makes sense is a reading, not a rule.
Alt text
A missing alt fails the audit. One that says nothing useful passes it.
Contrast beyond the actions
The theme gate holds every action fill against what is drawn on it. Text on surfaces, status colours and chart series are checked by eye, because the colour belongs to whoever owns the theme.