Accessibility: release checklist¶
The web UI targets WCAG 2.2 Level AA. That is a step past what EN 301 549
(the standard BITV 2.0 and the BFSG point at) requires today - WCAG 2.1 AA -
and the bar its next edition adopts. The public statement at
/legal/accessibility (leader/web/accessibility.py) says what conforms and
what does not; this page is how we keep it true.
Most of the work is automated and runs in CI on every change. The rest needs a person, a screen reader and about an hour, once per release.
What the machines check¶
| Check | Where | WCAG |
|---|---|---|
| axe-core, serious and critical rules, colour contrast on, every app page, dark and light theme | scripts/frontend_smoke.mjs |
many, incl. 1.1.1, 1.3.1, 1.4.3, 4.1.2 |
| No horizontal scroll at 320 CSS px | smoke test | 1.4.10 |
| No cropped table at 320 px: every table stacks into cards or scrolls inside a focusable, labelled region; no cell clips its text | smoke test (a11y_checks.tableCropProblems) |
1.4.10 |
| Every pointer target is 24 x 24 CSS px, or a 24 px circle on it touches no other target; inline links in text are exempt. Open overlays too: account menu, toasts, a menu, a dialog | smoke test (a11y_checks.targetSizeProblems) |
2.5.8 |
| Tab forwards and back (30 stops each way, at 1400 px and 320 px): the focused element is never entirely under a sticky or fixed element (app bar, sticky table header, toasts) | smoke test (a11y_checks.focusObscuredProblems) |
2.4.11 |
| Keyboard-only walkthrough: register, create a pool, generate a join command, open inference, answer the session-timeout warning - with a focus indicator at every stop | scripts/keyboard_walkthrough.mjs |
2.1.1, 2.4.3, 2.4.7, 2.4.11, 2.2.1 |
| Contrast of every token pair in both themes, 3:1 for field borders and the focus ring, nothing below 12 px | tests/unit/leader/web/test_design_tokens.py |
1.4.3, 1.4.11 |
Structural rules a page can break invisibly: one <main>, one <h1>, the skip link first, no outline: none on :focus, no per-tick live regions, chart text equivalents, consistent help, no drag-only controls |
tests/unit/leader/web/test_accessibility.py |
1.3.1, 2.4.1, 2.4.7, 3.2.6, 2.5.7, 4.1.3 |
Run them locally:
npm i --no-save puppeteer-core axe-core # once; uses the system Chromium
node scripts/frontend_smoke.mjs # ~15 min; SMOKE_ONLY=<regex> for one page
node scripts/keyboard_walkthrough.mjs # ~30 s; WALK_VERBOSE=1 prints every stop
.venv/bin/python -m pytest tests/unit/leader/web -q
The smoke test prints one line per page and theme; a target-size,
focus-obscured or reflow@320 line names the element and what it collides
with. The walkthrough prints each step, how many Tab presses it took, and
every problem it met.
What a person checks, per release¶
The machines cannot hear what a screen reader says, judge whether an announcement makes sense, or see a Windows contrast theme. Walk this flow by hand before a release:
- Register a new account (
/register), sign in. - Create a pool from the dashboard.
- Add a node: generate the join command, copy it.
- Load a model on the inference page (needs a fellow with a GPU and an exported model; on a test leader, use a seeded one).
- Chat: send a message, wait for the reply, start a new chat.
Do the whole flow in each of these setups:
| Setup | What to look for |
|---|---|
| Keyboard only (no mouse, no touchpad) | Every control reachable with Tab / Shift+Tab, operable with Enter / Space / arrows; the focus ring always visible and never hidden; dialogs take focus and give it back; Escape closes menus and dialogs. |
| NVDA + Firefox (Windows) | Page title and the <h1> are announced on arrival; landmarks (main, navigation) are listed; every field reads its label and hint; errors are read when they appear; the training and model status are announced as occasional summaries, not a stream; a finished chat reply is announced once. |
| VoiceOver + Safari (macOS, and iOS once) | The same as NVDA. On iOS also: the drawer opens from the app bar, and rotor navigation by headings works. |
| 200% text zoom (browser setting "text only", or 200% page zoom at 1280 px) | No text cut off or overlapping, no control pushed off-screen, no horizontal scroll except inside table regions. |
| Windows High Contrast (a dark and a light contrast theme) | Every button, field, card and tab boundary is drawn; the focus ring and the selected tab are visible; icons are visible; status is still told by text, not colour. |
What to record¶
Write the result into the release notes (or the PR that bumps the version), one row per setup:
- date, version or commit, who tested;
- browser + assistive technology and their versions (e.g. "NVDA 2026.3, Firefox 142");
- pass, or the barriers found: page, step, what happened, what was expected, the WCAG criterion if known;
- for each barrier, the issue it was filed as.
Then:
- fix what blocks the flow before the release; file the rest;
- update
leader/web/accessibility.py: the gaps under "Non-accessible content", what changed under "What is accessible", andSTATEMENT_REVIEWED(the Leichte Sprache page takes its date from it); - if a manual run found something a machine could have caught, add the check
to
scripts/a11y_checks.mjsortest_accessibility.py.
Rules worth knowing before you build¶
The full list lives in src/silent_swarm/leader/CLAUDE.md (Accessibility) and
the UI design system. The R7 additions:
- Streams announce through the throttle. Anything updated by SSE or a poll
is plain text, never
aria-liveorrole="status"/role="log"; say what changed withSS.announce(text, {key, every, quiet})- one sentence per key pereveryms, latest wins, first render quiet. - Sticky things need scroll padding. A new sticky bar or header must leave
room for the control the browser scrolls under it (
scroll-padding-top). - Targets are 24 px. Use the button classes (
.btn-smis 32 px); a row of small text links that can wrap getsmin-height: 24px. - Charts get a sentence and the data. A
role="img"label that describes the data, and a table of the numbers on request. - Nothing only by dragging. A drop target is also a click target; a draggable view also has another way to the same result.