Skip to content

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:

  1. Register a new account (/register), sign in.
  2. Create a pool from the dashboard.
  3. Add a node: generate the join command, copy it.
  4. 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).
  5. 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", and STATEMENT_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.mjs or test_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-live or role="status" / role="log"; say what changed with SS.announce(text, {key, every, quiet}) - one sentence per key per every ms, 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-sm is 32 px); a row of small text links that can wrap gets min-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.