Skip to content

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.

  • 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-sig for 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 --border as the boundary between adjacent signal fills — it is what keeps a keyed contact sheet legible.
  • SHOULD treat the mono --muted label color as the floor for secondary text; do not introduce a fainter grey.

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 the phosphor (monochrome) theme and for colour-blind operators.
  • MUST keep the system fully usable in the phosphor theme, 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-work directly. 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. --focus tracks --sig-work exactly in dark, extreme and phosphor, and darkens to #00627a in light — same hue, 5.38:1 at its worst surface. A brand that re-keys --sig-work therefore 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 over divs with handlers.
  • 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; see references/motion.md).

Corruption is decorative; it must never reach assistive tech.

  • MUST let the clean string own the accessible name (aria-label or the real text node), and mark the corrupted/zalgo layer aria-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.
  • MUST give each cat a text equivalent naming the session and its state (e.g. aria-label="nova — waiting") or mark it aria-hidden when an adjacent text label already carries that information.
  • NEVER rely on the sprite pose as the only indication of state.

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.

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.)

  • Dialog overlays (Modal, Drawer, Command palette) render on the native <dialog> element opened with showModal(), 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 gets aria-controls and each panel is a role="tabpanel" with aria-labelledby back 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 Esc without moving focus, stay open while hovered/focused, and re-show on the next hover/focus. Dialog overlays dismiss on Esc natively.

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.