How to actually test accessibility

Applies to a11y.axe-clean, a11y.keyboard-operable, a11y.focus-visible.

Covers a11y.axe-clean, a11y.keyboard-operable, a11y.focus-visible.

The number that matters

Automated tools detect roughly a third of WCAG failures. Everything in the other two thirds is about meaning and behaviour: whether the alt text says the right thing, whether the focus order matches the reading order, whether an error message helps. A clean axe run is where testing starts.

Run more than one engine if you can. axe-core and HTML_CodeSniffer disagree enough that running both finds materially more than either alone.

The automated pass

npx @axe-core/cli https://example.com --tags wcag2a,wcag2aa

Sample deliberately: the home page, a listing with filters, a detail page, a page with a form, and any page with a modal or a carousel. Five pages chosen this way beat fifty chosen at random, because template coverage is what matters, not page count.

The keyboard pass, which is the real test

Put the mouse down. This takes about ten minutes per site and finds more than any scanner.

  1. Tab from the top. The first stop should be the skip link. Activate it and confirm the next Tab lands in page content, not back in the header.
  2. Tab through the whole page. Watch for three things: a stop you cannot see, a stop on something that is not interactive, and an order that does not match the visual order.
  3. Open every menu with Enter or Space. Arrow keys where the pattern calls for it. Escape must close it and return focus to the trigger.
  4. Open a modal. Focus moves inside. Tab cycles inside and never escapes behind it. Escape closes. Focus returns to whatever opened it.
  5. Operate the carousel, the filters, the video controls, the custom select. Anything a mouse can do, a keyboard must do.
  6. Submit an empty form. Focus should land on the first error, and the error should be text.

The failure you will hit most often: focus disappearing into an off-screen menu that is hidden with opacity: 0 or transform instead of display: none, visibility: hidden or the hidden attribute. Visually gone, still focusable.

Focus indicators

Define one style in the base layer:

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

Per-component focus styling leaves every component nobody remembered with the browser default, and outline: none with no replacement is a failure, not a design decision. The indicator needs 3:1 contrast against whatever sits behind it, which is worth checking over images and gradients.

A screen reader pass, when the budget allows

Thirty minutes with VoiceOver on macOS (Cmd+F5) or NVDA on Windows tells you more than any report. Navigate by heading, then by landmark, then by link. If you cannot work out where you are from those three lists alone, neither can anyone else.