Skip to main content

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 lint passes 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.