Accessibility
Control Room is high-contrast and loud by nature, but loud is not the same as accessible. The aesthetic and WCAG 2.1 AA coexist by design; where they would conflict, accessibility wins. These are hard requirements, not suggestions.
Contrast
Section titled “Contrast”- MUST meet WCAG 2.1 AA: 4.5:1 for text under 18px (or under 14px bold), 3:1 for large text and for meaningful UI/graphic boundaries.
- MUST pair every signal fill with
--on-sigfor text/icons placed on it — except error fills (--sig-err), which use--on-err(white in light, dark elsewhere). Both are tuned per theme to clear AA; when you add a signal hue, re-verify the pair in all four themes before shipping. - MUST keep the near-black
--borderas the boundary between adjacent signal fills — it is what keeps a keyed contact sheet legible. - SHOULD treat the mono
--mutedlabel color as the floor for secondary text; do not introduce a fainter grey.
Color is never the only channel
Section titled “Color is never the only channel”Color carries state (Law 2), so it must be backed up:
- MUST pair every StatusDot / keyed region with a text or shape equivalent —
an
aria-label, a visible label, or one of the Law 4 diagonal primitives. - SHOULD reach for the severity shape (
.cr-sev--*, Law 4) when a state needs a non-colour reading: the polygon’s side-count encodes danger independent of hue, so it is the built-in backup for thephosphor(monochrome) theme and for colour-blind operators. - MUST keep the system fully usable in the
phosphortheme, which is effectively monochrome — if a screen only works because of hue, it is broken.
- MUST keep a visible focus indicator. The system default is already set in
control-room.css:*:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } - MUST draw the ring in
--focus, never in--sig-workdirectly. The ring has its own token precisely so it can be tuned for contrast independently of the working state: WCAG 2.4.11 Focus Appearance requires the indicator to clear 3:1 against the adjacent surface, and in the light theme--sig-work(#0891b2) reaches only 2.86:1 against--board.--focustracks--sig-workexactly in dark, extreme and phosphor, and darkens to#00627ain light — same hue, 5.38:1 at its worst surface. A brand that re-keys--sig-worktherefore cannot silently break its own focus ring. - NEVER remove focus outlines. If a component needs a custom focus style, it must be at least as visible as the default and use a signal hue.
- MUST keep a logical tab order and standard keyboard operation for all
interactive components (Button, Nav links, controls). Native elements
(
<button>,<a>) are strongly preferred overdivs with handlers.
Motion
Section titled “Motion”- MUST honor
prefers-reduced-motion: reduce— enforced globally, but every component must also remain fully legible with motion off (state via color + shape + text, never motion alone; seereferences/motion.md).
Glitch, decay, and cursed text (Law 3)
Section titled “Glitch, decay, and cursed text (Law 3)”Corruption is decorative; it must never reach assistive tech.
- MUST let the clean string own the accessible name (
aria-labelor the real text node), and mark the corrupted/zalgo layeraria-hidden="true". - NEVER apply glitch to numerals, labels, or anything under 18px — this is both a design law and an a11y rule (it destroys legibility).
- MUST cap combining marks at 2 per glyph so corrupted text cannot overflow and cover adjacent content.
Canvas sprites (the pixel-cat)
Section titled “Canvas sprites (the pixel-cat)”- MUST give each cat a text equivalent naming the session and its state
(e.g.
aria-label="nova — waiting") or mark itaria-hiddenwhen an adjacent text label already carries that information. - NEVER rely on the sprite pose as the only indication of state.
Automated gate
Section titled “Automated gate”Accessibility is enforced by CI, not just documented. pnpm run test:a11y
(Playwright + axe-core) loads the living gallery in all four themes and fails
on any serious/critical WCAG 2.1 A/AA violation (scoped to the component
region). It runs on every push (.github/workflows/deploy.yml) and blocks the
deploy. A companion visual-regression check (pnpm run test:visual) snapshots the
gallery per theme; run pnpm run test:visual:update after intentional changes.
Direction (RTL)
Section titled “Direction (RTL)”Layout is direction-agnostic: the component CSS uses logical properties
(margin-inline-*, padding-inline-*, border-inline-*, text-align: start/end)
rather than physical left/right, so the whole system mirrors under
dir="rtl". A guard (pnpm run test:rtl) fails the build if a physical flow
property creeps back in, and the responsive gate checks there’s no horizontal
overflow under RTL. (Fixed/absolute overlay positioning and gradient/clip-path
angles are direction-neutral and out of scope.)
Focus management & keyboard
Section titled “Focus management & keyboard”- Dialog overlays (Modal, Drawer, Command palette) render on the native
<dialog>element opened withshowModal(), so the platform provides the focus trap and returns focus to the invoker on close — no custom trap to drift. - Popover moves focus into the panel on open and returns it to the trigger on
Esc/ close. - Roving focus / active-descendant is implemented for the composite widgets:
Tabs, Radio group, Segmented, Combobox, and the Command palette
(
aria-activedescendant+ arrow-key navigation), so each is a single tab stop. - Menu adds typeahead — printable keys focus the next item whose label matches (keys within ~600ms accumulate), alongside arrows / Home / End / Esc.
- Tabs wire the tab↔panel relationship when given an
id: each tab getsaria-controlsand each panel is arole="tabpanel"witharia-labelledbyback to its tab (and is focusable), so the pairing is exposed to assistive tech. - Content on hover or focus is dismissable (WCAG 1.4.13): Tooltip and Hover
card hide on
Escwithout moving focus, stay open while hovered/focused, and re-show on the next hover/focus. Dialog overlays dismiss onEscnatively.
Component acceptance (a11y slice)
Section titled “Component acceptance (a11y slice)”Before shipping, confirm:
- Text and UI boundaries meet AA contrast in dark / light / extreme / phosphor.
- Every signal use has a non-color backup (label / shape / aria).
- Focus is visible and keyboard operation works.
- Reduced-motion leaves the component fully legible.
- Decorative glitch/canvas is
aria-hidden; the clean string is the name.