Skip to content

Accessibility

The question: can everyone actually use it?

This is listed among the non-functional typologies by convention, and the convention is wrong. A form that cannot be completed with a keyboard is broken for a portion of users, in the same sense that a checkout that returns 500 is broken. It is a correctness question, and it belongs in the same suite as the other correctness questions.

Treating it as polish is what produces the annual audit: 400 findings, a remediation project, and the same 400 findings a year later — because nothing in the daily loop changed.

What automation catches, and what it does not

Section titled “What automation catches, and what it does not”

Automated rules catch roughly a third of the issues in practice. That third is worth having, because it is the mechanical third that would otherwise consume a human’s attention: missing alternative text, insufficient colour contrast, missing form labels, invalid ARIA, duplicate ids, missing document language, headings that skip a level.

The other two thirds require judgement:

  • Alternative text that exists but says image1.png, or describes the wrong thing.
  • A focus order that is technically valid and makes no sense.
  • A modal that traps focus correctly and never announces that it opened.
  • An error message that is visible and not associated with its field, so a screen reader user hears “invalid” attached to nothing.
  • A control that is reachable, operable, and completely unintelligible without the visual layout.

Anyone claiming a page is accessible because a scanner reported zero violations is reporting on the third.

Component level, automated, on every merge. Run the rule engine against each component in isolation, in its states — empty, filled, loading, error, disabled. This is the highest-value placement, because a component is where a violation is introduced and where it is cheap to fix, and because it is checked once rather than once per page that uses it.

Journey level, automated, in the end-to-end suite. Run the same engine at key points in each critical journey. Catches what only exists in composition: two components each fine, together producing a duplicate landmark or a broken heading order.

By hand, per feature. Three checks, ten minutes, and they find most of what matters:

  1. Unplug the mouse. Complete the whole journey with Tab, Shift-Tab, Enter, Space and the arrow keys. Can you reach everything? Can you see where you are? Can you escape the modal? Can you skip the navigation?
  2. Zoom to 200% and set the viewport to 320 px wide. Does the content reflow, or does it clip and scroll horizontally?
  3. Turn on a screen reader and complete one journey. VoiceOver on macOS, NVDA on Windows. Uncomfortable the first three times, then routine — and it is the single most informative thing anyone on the team can do.

There is a useful accident available here: writing end-to-end tests that address elements by role and accessible name rather than by CSS selector means the test suite fails when an element stops being reachable by an assistive technology.

getByRole('button', { name: 'Place order' }) cannot find a <div onclick> with no accessible name. So the accessibility regression breaks the functional test, in the ordinary pipeline, on the merge that introduced it — which is far earlier than any audit. This is the cheapest accessibility win available to a team that is already writing browser tests.

WCAG 2.2 level AA is the working target, and it is also what most jurisdictions’ legislation references — the European Accessibility Act and the public-sector directive in the EU, Section 508 in the US, and equivalents elsewhere. For a public-facing product this is a legal requirement rather than a preference, which makes it exactly the kind of quality attribute that belongs in the requirements with a threshold and an owner.

The threshold: automated rules fail the build. Manual findings are triaged like any other defect, by severity — a keyboard trap on the checkout is critical, a contrast ratio of 4.4 against a required 4.5 on a disabled label is not.

axe-core is the rule engine underneath nearly everything worth using; the per-framework integrations are thin wrappers around it and can be run from the component test suite and from Playwright. Manual: VoiceOver, NVDA, the browser’s accessibility inspector, and the keyboard already attached to the machine. See browser and mobile tooling.