Accessibility
Principle 05 says leave no one behind — so access isn't a coat of paint, it's the floor we build on. The target is WCAG 2.2 AA. Everything below is built into the system once, so every component inherits it.
Built-in, system-wide
Four things every page gets for free, from the shared base layer.
Skip link
Press Tab on any page — "Skip to content" appears first and jumps past the nav.
Decorative icons
Every Lucide icon is aria-hidden — screen readers never announce decoration.
Reduced motion
prefers-reduced-motion collapses animations, transitions, and smooth scroll.
Landmarks
Banner, navigation, and main landmarks on every page for structured navigation.
Focus
A single, high-contrast focus ring (token --focus-ring) on every interactive
element via :focus-visible — visible for keyboard users, quiet for mouse users.
Overlays (modal, drawer, menu, omnibox) trap focus while open, close on Esc, and return focus to the trigger when dismissed.
Keyboard
Every interaction is reachable and operable from the keyboard, in a logical order.
| Key | Action |
|---|---|
| Tab / Shift+Tab | Move forward / back between controls |
| Enter / Space | Activate a button; toggle a checkbox, switch, or row |
| Esc | Close a modal, drawer, menu, or omnibox; cancel |
| ↑ ↓ ← → | Move within tabs, menus, radio groups, the tree, and a workflow's steps |
| Home / End | Jump to first / last item in a list or menu |
| ⌘/Ctrl+K | Open the command omnibox from anywhere |
Screen readers
Meaning is conveyed in the accessibility tree, not just visually.
- Status is text, not just color or shape. A status dot reads "● Watch", not a bare hue — the word ships with it.
- Icon-only controls carry an
aria-label(close, more, settings, pager, theme) so they announce a purpose. - Live updates use live regions — toasts and inline banners are
role="status"(aria-live="polite"); errors and overdue alerts arerole="alert". - Visually-hidden text via the
.sr-onlyutility supplies labels and context that don't need to be seen.
<button class="btn btn--icon" aria-label="More actions"><i data-lucide="ellipsis"></i></button>
<label class="field__label">Bank name <span aria-hidden="true">*</span><span class="sr-only">(required)</span></label>
<div class="toast" role="status" aria-live="polite">…</div>ARIA patterns by component
The roles and attributes each interactive component ships with.
| Component | Roles & attributes |
|---|---|
| Modal / dialog | role="dialog" · aria-modal="true" · aria-labelledby + aria-describedby · focus trapped · Esc closes · focus returns |
| Tabs | role="tablist" / tab · aria-selected · arrow-key navigation · panel role="tabpanel" |
| Menu | role="menu" / menuitem · arrow keys · Esc closes · focus returns to trigger |
| Search / omnibox | role="combobox" · aria-expanded · aria-controls a listbox of options · arrow keys + Enter |
| Tree | role="tree" / treeitem · aria-expanded on parents · arrow keys |
| Accordion | header is a button with aria-expanded + aria-controls for its panel |
| Progress / meter | role="progressbar" · aria-valuenow / aria-valuemin / aria-valuemax |
| Toast / banner | role="status" (polite) for info/success; role="alert" for errors & overdue |
| Breadcrumbs | <nav aria-label="Breadcrumb"> · current page marked aria-current="page" |
| Step navigation | completed/active/upcoming exposed via aria-current="step" + visually-hidden status |
Forms
Every field's label, hint, and error are programmatically tied to the input.
<label class="field__label" for="rt">Routing number</label>
<input class="input" id="rt" aria-invalid="true" aria-describedby="rt-err">
<span class="field__error" id="rt-err">Must be 9 digits.</span>for/idtie label to control — the label is clickable and announced.aria-describedbyconnects hints and errors so they're read with the field.aria-invalidmarks the error state;requiredis real, not just a "*".- Checkboxes, radios, and switches wrap their input in the label — associated by construction.
Motion & target size
Reduced motion
Spinners, skeletons, the agentic-streaming shimmer, and all transitions stop when the OS requests reduced motion.
Target size
Interactive targets meet the 24px minimum; default controls are 30–38px with comfortable hit areas.