Accessibility (WCAG 2.2 AA)¶
The dashboard targets WCAG 2.2 AA. Accessibility is realized mostly through the F3 design system and a set of reusable primitives, so new surfaces inherit it rather than retrofitting each page.
- Feature: F18 · Design: ADR 0068 (
design/adr/0068-dashboard-accessibility.md) · Spec:design/vision/specs/F18-accessibility.md - Frontend:
lib/a11y.ts(pure),hooks/useFocusTrap.ts,hooks/announcer.ts+hooks/useAnnouncer.tsx,components/SkipLink.tsx,index.css(reduced-motion)
What ships¶
| Concern (WCAG) | Mechanism |
|---|---|
| Keyboard operability (R2) | useFocusTrap traps + restores focus in dialogs; SkipLink → #main landmark |
| Screen-reader live regions (R3) | AnnouncerProvider polite/assertive aria-live; RouteAnnouncer announces page changes |
| Contrast AA (R4) | lib/a11y.ts OKLCH→luminance contrast math; a CI test audits the real F3 tokens meet AA |
| Non-colour status (R4) | F3 status semantics — always icon + label, never colour alone |
| Reduced motion (R5) | global @media (prefers-reduced-motion: reduce) neutralizes animation/transition |
| Accessible charts (R6) | F4 <ChartFrame> renders a keyboard-reachable data-table fallback |
Keyboard model¶
- Skip link — the first tabbable element; focusing it reveals a “Skip to main content” link that
jumps to
<main id="main" tabIndex={-1}>. - Dialogs — the ⌘K command palette uses
useFocusTrap(ref, open): focus moves into the dialog on open, Tab/Shift-Tab wrap inside it, and focus returns to the trigger on close. - Every actionable control is a real
<button>/<a>/<input>— reachable and operable without a mouse.
Announcing live content¶
import { useAnnouncer } from '@/hooks/announcer'
function DriftWatcher() {
const { announce } = useAnnouncer()
// when an F8 push arrives:
announce(`Drift critical on ${model}`) // polite — heard without stealing focus
announce('Service down', 'assertive') // assertive — interrupts
}
The provider mounts one hidden aria-live region per priority. announce() clears then re-sets the
text so repeating the same message still triggers a screen-reader announcement.
Contrast audit (the axe stand-in)¶
lib/a11y.test.ts converts each F3 OKLCH token to relative luminance and asserts foreground, muted,
and primary all meet AA (≥ 4.5:1) on their background across the day/night/high-contrast themes. Editing
a token that regresses contrast fails CI — a dependency-free guard until axe-core is added.
oklchLuminance implements OKLCH → OKLab → linear-sRGB → WCAG relative luminance (channels clamped to
gamut), so the check runs on the real palette, not a hex approximation.
Notes & limits¶
This slice ships the substrate + first adoptions (palette focus trap, skip link, route announcer, reduced-motion, token contrast audit). Deferred (tracked in the plan): axe-core in CI (R7 — adds a dependency; the token audit is the interim guard), a full manual audit with tracked issues (R1), and focus-trap adoption across every remaining dialog/drawer.
See docs/dashboard/architecture.md for
the design diagram.