Testing & linting for accessibility
Two automated layers catch accessibility regressions here: ESLint on every file at commit time, and axe inside Storybook component tests. Neither replaces keyboard-testing a change yourself, but between them they catch the common mistakes before review.
ESLint — eslint-plugin-jsx-a11y
Configured in eslint.config.js. It starts from jsxA11y.flatConfigs.recommended and then
pins ~20 rules explicitly. The pinned severities are the part worth knowing, because they
encode what this team treats as a hard failure versus a prompt:
error — these block: alt-text, anchor-is-valid, aria-props, aria-proptypes,
aria-role, aria-unsupported-elements, heading-has-content, html-has-lang,
img-redundant-alt, label-has-associated-control, lang, no-access-key,
no-distracting-elements.
warn — these don't: click-events-have-key-events, interactive-supports-focus,
media-has-caption, mouse-events-have-key-events, no-autofocus.
That split is deliberate but has a consequence: the two rules that catch a div-with-onClick
being unreachable by keyboard are warnings. click-events-have-key-events and
interactive-supports-focus will not fail your build. If you add a click handler to a
non-button, nothing stops you — see Keyboard & focus for what to do
instead.
npm run lint # everything, cached
npm run lint:fix # autofix what's fixable
npm run lint:a11y # a11y-only subset (alt-text, anchor-is-valid, aria-props)
Storybook + axe
@storybook/addon-a11y is registered in .storybook/main.ts, and
.storybook/vitest.setup.ts wires its annotations into the Vitest project:
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
vitest.config.ts runs those stories through storybookTest in a real headless Chromium
via Playwright — not jsdom. That matters: contrast, focus visibility, and computed ARIA need a
real rendering engine to evaluate honestly.
Interactive checking during development:
npm run storybook # port 4000 — the Accessibility tab runs axe live
Before you open a PR
npm run lintpasses with no new warnings — the a11y warnings are easy to ignore precisely because they don't fail; don't.- Tab through your change start to finish without touching the mouse. Every interactive element reachable, focus ring visible, order matches visual order.
- Anything that opens over the page (modal, drawer, popover) traps focus and returns it on close.
- New or changed colours checked against the token palette — see Colour & typography.
- If you added a component, add a story.