Accessibility Testing
Date: 2026-08-17
Automated checks catch roughly a third of accessibility issues — the mechanical third. The remaining share needs a keyboard, a screen reader and judgement, and a clean automated scan is routinely mistaken for conformance.
Accessibility testing verifies that people using assistive technology, keyboards, magnification or alternative input can complete tasks.
What automation catches
RELIABLY DETECTED
missing alt attributes
insufficient colour contrast
— Colour Contrast
form inputs without labels
— Accessible Forms
invalid or conflicting ARIA
missing document language
duplicate ids
empty buttons and links
See: Colour Contrast · Accessible Forms
These are mechanical and worth automating, because they’re common, cheap to check, and there’s no reason to ship one.
What automation cannot catch
IS THE ALT TEXT MEANINGFUL?
alt="image1.jpg" passes
IS THE FOCUS ORDER LOGICAL?
every element focusable, in a
nonsensical sequence, passes
DOES THE PAGE MAKE SENSE READ ALOUD?
IS THE ERROR MESSAGE HELPFUL?
CAN THE TASK ACTUALLY BE COMPLETED?
IS THE HEADING STRUCTURE MEANINGFUL?
h1 → h4 → h2 is flagged; h1 → h2
with wrong content isn't
[CHECK: current published figures for the proportion of issues automated tooling detects before citing a specific number — the commonly-quoted figures come from particular studies and vary.]
The criteria requiring judgement are the ones that block people, which is why a clean scan is a starting point rather than a result — WCAG.
The layers
1 LINT AS YOU WRITE
eslint-plugin-jsx-a11y
← cheapest, earliest — Linting
2 UNIT / COMPONENT
axe assertions in component tests
3 E2E PAGE SCANS
axe against real rendered pages
— End-to-End Testing
4 KEYBOARD WALKTHROUGH
manual, 15 minutes, highest yield
— Keyboard Navigation
5 SCREEN READER WALKTHROUGH
manual — Screen Readers
6 TESTING WITH DISABLED USERS
the only test that tells you
whether it works
See: Linting · End-to-End Testing · Keyboard Navigation · Screen Readers
Layer 4 finds more real problems per minute than 1–3 combined, and needs no tooling — unplug the mouse and try to buy something.
Automating it in tests
import { axe } from 'vitest-axe'
test('checkout form has no axe violations', async () => {
const { container } = render(<CheckoutForm />)
expect(await axe(container)).toHaveNoViolations()
})Component-level scanning is better than page-level — failures localise, and it runs on every change rather than on every deploy.
Query by role in ordinary tests too, which makes every functional test a partial accessibility test at no extra cost:
screen.getByRole('button', { name: 'Add to basket' })If that query fails, the control isn’t reachable by assistive technology — the test catches it without anyone writing an accessibility test — Integration Testing.
The manual checklist
Fifteen minutes, per significant change:
□ Tab through — everything reachable?
□ Focus visible at every step?
— Focus Management
□ Escape closes what it should?
□ Any keyboard traps?
□ Complete a purchase, keyboard only
□ Zoom to 400% — does it reflow?
□ prefers-reduced-motion on
— Reduced Motion
□ Screen reader through one journey
□ Greyscale screenshot — anything
ambiguous?
See: Focus Management · Reduced Motion
“Complete a purchase, keyboard only” is the acceptance test. Everything above it is diagnosis.
Third-party components
The most-missed risk, and it’s yours regardless of who wrote it:
cookie banners
chat widgets
payment iframes
review plugins
A cookie banner trapping focus blocks your entire site for keyboard users before they see anything — and “it’s the vendor’s code” is not a defence to a customer who can’t buy, or to a regulator — Accessibility Law in the UK, Third-Party Scripts.
Test them. They’re rarely covered because they’re not in the codebase.
Where it belongs in the pipeline
BLOCKING lint rules, component
axe assertions
→ fast, deterministic
REPORTING full-page scans
→ a trend, with a ratchet
MANUAL, PER keyboard walkthrough on
RELEASE changed journeys
PERIODIC full audit, ideally
external
Ratchet the page-level scans rather than blocking on the existing backlog — fail on new violations, fix the rest opportunistically. Blocking on a full fix means it never starts — Static Analysis.