diff --git a/docs/A11Y.md b/docs/A11Y.md new file mode 100644 index 0000000..8d986f5 --- /dev/null +++ b/docs/A11Y.md @@ -0,0 +1,113 @@ +# Accessibility pass + +A pass over every route the PWA serves (the routes in [STATES.md](STATES.md)) +for launch, on 2026-10-10. Each route was checked for: + +- **Keyboard** — every action reachable and operable with the keyboard alone. +- **Focus** — a visible focus ring on everything that takes focus. +- **Names and roles** — what a screen reader announces: switches, checkboxes, + the current nav item, dialogs and their labels. +- **Contrast** — text at least 4.5:1 against its ground, for every token pair + used for text. +- **Hit targets** — every tap target at least 44 × 44 px. +- **Reduced motion** — what `prefers-reduced-motion: reduce` turns off. + +The pass read each screen's markup and stylesheets, computed contrast from +[tokens.css](../web/src/styles/tokens.css), and checked the fixes' computed +styles in Chrome. Severity: **high** blocks a task for keyboard or +screen-reader users; **medium** fails a criterion above but leaves a way +through; **low** is a gap outside the criteria above, or a polish item. High +and medium findings are fixed in this pass with a regression test; low ones are +follow-ups. + +## Shared building blocks + +Most of the criteria are met once, in the shared components every screen uses: + +| Concern | Where | Behaviour | +| --- | --- | --- | +| Focus ring | [base.css](../web/src/styles/base.css) | `:focus-visible` draws a 2 px grape (`--color-accent`) outline 2 px out, on every focusable element. | +| Dialogs | [Sheet](../web/src/ui/Sheet.tsx) | `role="dialog"`, `aria-modal="true"`, and a required `aria-label` or `aria-labelledby` (the type will not compile without one). Opening moves focus in, Tab is trapped, Escape and the scrim close, the page behind is `inert`, and focus returns to the opener. Every sheet is labelled by its visible title. | +| Toggles | [Toggle](../web/src/ui/Toggle.tsx) | A `