Accessibility here
The target is WCAG 2.2 Level AA, and the WCAG 2.2 quick reference is the one external page worth bookmarking. The rest of this section is not a WCAG tutorial — it is what those criteria turn into in this codebase: a dark-first, map-heavy operator console where a dispatcher is watching a fire.
That context changes the priority order. This app has almost no long-form text and almost no forms; it has controls layered over images. So the criteria that bite here are keyboard operability, accessible names on icon-only buttons, live regions for state that changes without a click, and non-text contrast on status colors — not reading order or captions.
Read the enforcement honestly
Two facts to know before you trust a green build.
eslint-plugin-jsx-a11y is configured and it is finding real problems. As of this writing,
eslint components reports 125 jsx-a11y findings across 34 files — and every one of them
is a warning, not an error. npm run lint exits 0. CI (devops/jobs/pr-job.yml) runs plain
npm run lint with no --max-warnings, so none of it blocks a merge.
46 jsx-a11y/click-events-have-key-events (warn)
39 jsx-a11y/no-static-element-interactions (warn)
27 jsx-a11y/no-noninteractive-element-interactions (warn)
7 jsx-a11y/interactive-supports-focus (warn)
4 jsx-a11y/no-noninteractive-element-to-interactive-role (warn)
The axe integration never fails either. @storybook/addon-a11y is installed and wired into
the Vitest browser project, but .storybook/preview.tsx sets a11y: { test: 'todo' }, which
reports violations without failing anything. There are 7 story files in the whole repo, and no npm
script runs that Vitest project at all. Details in Testing and linting.
So: the tooling here is advisory. Treat a jsx-a11y warning on a line you touched as a blocker even though the build won't.
What the app already does well
Copy these patterns rather than inventing new ones.
Live regions for state that changes on its own. role="status" with aria-live="polite"
appears in AreaScopeBar, CameraMovementZoom, CameraListComponent, and atoms/Loader — the
camera count, the zoom readout, and the loading state all announce.
Toggle buttons carry their own state. organisms/PeakFinderOverlay/PeakFinderControls.tsx and
molecules/AreaFilter/AreaFilterSection/AreaFilterSection.tsx use real <button> elements with
aria-pressed, so the pressed state is native rather than visual-only.
Accessible names on icon-only controls. There are ~204 aria-label attributes under
components/, which is the right instinct for a UI that is mostly icons.
The four rules that matter in this repo
- If it responds to a click, it is a
<button>. The single largest source of a11y debt here isonClickon adivor a MUI icon. See Markup and ARIA. - Every icon-only control needs an
aria-label; every decorative icon needsalt="".jsx-a11y/alt-textchecks thataltexists, not that it says anything useful. - State that changes without a keypress needs a live region that was already mounted. A region rendered in the same commit as its first message announces nothing.
- Use the tokens; don't hand-pick a color. Several existing tokens fail contrast at their current usage — Color and typography has the measured numbers.
Before you open a PR
Run these, in this order:
npm run lint # read the jsx-a11y warnings on YOUR files, not just the exit code
npm run test:ci # jest — this is what CI runs
npm run storybook # only if you touched a component that has a story
Then do the manual pass. It takes about two minutes and catches what no tool in this repo catches.
- Tab through your change. Every control you added is reachable, in visual order, and
shows a visible focus ring. If it has no ring, add one — there is no global
:focus-visiblestyle instyles/global.cssorstyles/global.scss. - Activate everything from the keyboard.
Enteron buttons and links,Spaceon buttons and checkboxes,Escapecloses whatever you opened. - Nothing is a focus dead end. After a dialog closes, focus lands somewhere sensible —
not on
<body>. - Every new icon-only button has an
aria-labelnaming what it does, not what it looks like ("Show peaks", not "eye-icon"). - Every new
<img>either describes content or isalt="". A filename is neither. - New colors come from
styles/tokens/, and if a color conveys status it clears 3:1 against its actual background. - No new text below 12px, and nothing smaller than the body text around it.
-
npm run lintproduced no new jsx-a11y warnings in the files you touched.
If your change adds a dialog, a menu, or anything with arrow-key navigation, read Keyboard and focus first — that page documents the two patterns this repo gets wrong most often.
Next: Markup and ARIA.