The four engineering practices that move the needle
Opinionated. Not exhaustive. The patterns that, when followed, eliminate most accessibility regressions before code review.
Semantic HTML first, ARIA second
When native HTML can do the job, use it. <button>
has keyboard handling, focus, role and Enter/Space activation built
in; <label> binds a control to its description;
<dialog> ships with the focus-trap and inert-rest-of-page
behaviour you'd otherwise hand-roll; <details> is
a disclosure widget with no JavaScript at all. ARIA is the escape
hatch — useful when there is no native element for the job, harmful
when sprinkled on top of one that already works. Reference: the
WCAG 2.2 success criteria and
the W3C ARIA Authoring Practices Guide.
Keyboard support is not a checkbox
Every interactive element must work with the keyboard alone. Tab
and Shift+Tab to move; Enter or Space to activate; Esc to dismiss
a transient surface (modal, popover, menu). Focus must be visible
— never outline: none; without a replacement. Focus
must move sensibly — when a modal opens, focus moves into it; when
the modal closes, focus returns to the element that opened it.
Test with the keyboard before you test with the mouse;
the mouse hides bugs the keyboard immediately exposes.
Accessible names + descriptions
Not aria-label sprinkles. The accessible name comes
from the element's text content by default; aria-labelledby
references another node's text; aria-label overrides
everything with a hard-coded string (and should be your last
resort because it bypasses translation and tends to drift from
the visible label). The accessible description is separate —
aria-describedby references the help-text or
error-message node. Verify with the DevTools accessibility tree
what the screen reader actually receives — assumptions and
reality often disagree.
Test in your real CI, not just locally
A local axe check is a sanity test. A green CI is a regression
gate. Wire eslint-plugin-jsx-a11y into every PR;
add axe-core assertions to unit and e2e tests; run
AT-driver flows on representative pages so a real screen reader
weighs in. The
screen-reader testing tools
survey covers what's worth automating and what's still a manual sweep.
The toolchain — 13 curated tools and libraries
Each entry maps to a lifecycle stage — lint, unit-test, e2e, runtime, monitoring or manual review — so you can wire the right tool into the right gate.
-
Runtime
axe-core
The accessibility test engine that powers most of the tooling below. Embed it in unit tests, e2e suites, or hook it into your dev-time DOM for runtime checks.
-
E2E
axe DevTools
Browser extension with Playwright, Cypress, Jest and Selenium integrations. The default developer-facing UI on top of axe-core.
-
Static-analysis lint rules for React JSX. Catches the easy wins (missing alt, invalid roles, no-onClick-on-div) before code review.
-
Runtime
vue-axe
Vue runtime accessibility checker — surfaces axe-core violations in the browser console as you develop.
-
Unit test
@testing-library/jest-dom
Accessibility-first query matchers for unit tests. Encourages you to look up nodes the way a screen reader would (by role + accessible name) rather than by class.
-
End-to-end browser automation with first-class axe-core integration. Run accessibility assertions across real browsers in CI.
github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright ↗
-
Unit test
Storybook a11y addon
Per-component axe-core scan inside your design system. Catches violations before they ship into product code.
-
Monitoring
Pa11y
CLI scanner that wraps axe-core and HTML CodeSniffer. The right primitive for a CI gate that fails the build on regressions.
-
Monitoring
Lighthouse CI
Google-driven, includes an accessibility audit (axe-core under the hood) plus performance and best-practices scores. Useful for trend tracking.
-
Manual
WebAIM WAVE
Visual scanner that overlays issues on the live page. Good for designers and content authors who don't want to wade through a JSON report.
-
Unit test
Wallaby + axe-core integration
IDE-feedback loop — runs axe-core assertions next to your test cursor. Tightens the developer iteration speed when wiring up a new component.
-
W3C-incubated standard for driving real screen readers from automated tests. The frontier of automated accessibility testing — finally moving past axe-style static checks.
-
Manual
NVDA + JAWS + VoiceOver
The actual screen readers your users use. No amount of automation replaces manual screen-reader review for the 30–40% of WCAG issues that automation can't catch.
Framework-specific guides
The accessibility gotchas that change shape between ecosystems — and the deeper coverage on each.
React
Keys on lists (mis-keyed lists confuse screen readers when items
reorder). Focus management on route change (without it, focus
stays on the link that triggered the navigation and the new page
is invisible to AT users). Portals and focus traps — a modal
rendered into document.body still needs to keep
focus inside itself. Hydration mismatches that change DOM
structure between SSR and client can silently break ARIA
relationships. Our deep dive on
aria-live regions in modern frameworks
covers the live-region announcement pattern that React's
reconciliation tends to break.
Vue / Svelte / Solid
Similar patterns; reactive state changes that don't fire
live-region updates by default. Each framework's reactivity
model has a different definition of "the DOM has changed", and
screen readers see the resulting node update — or don't — in
subtly different ways. Vue's v-if versus
v-show, Svelte's reactive declarations, and Solid's
fine-grained reactivity all produce different accessibility-tree
updates for what looks like the same code.
Native mobile (iOS + Android)
A completely different API surface. iOS exposes UIAccessibility
(and SwiftUI's .accessibilityLabel() /
.accessibilityHint()) to VoiceOver; Android exposes
AccessibilityNodeInfo to TalkBack. Web-style ARIA does not
translate. The
mobile native accessibility APIs
piece maps the web concepts to their native counterparts so
web-trained engineers can stop guessing.
Component libraries
Headless libraries (Radix UI, Headless UI, React Aria) handle the hard parts — focus management, keyboard handling, ARIA wiring — and leave the visual design entirely to you. Full-featured libraries (Material UI, Chakra, Ant) ship with opinionated visuals but vary widely in accessibility quality, and "accessible by default" rarely means "audited against real screen readers". Our accessible component libraries survey rates the major options against actual AT testing.
PR-review checklist for accessibility
Print, paste into your PR template, or wire into a bot. The minimum every reviewer should glance at.
- Interactive elements work with keyboard alone (Tab + Enter + Space + Esc).
- Focus indicator visible on every interactive element.
- Form inputs have an associated
<label>. - Error messages are associated with their inputs (
aria-describedbyor adjacent text). - Dynamic content changes announce via
aria-livewhen appropriate. - Modal dialogs trap focus and restore it to the opener on close.
- Images have meaningful alt text; decorative images carry
alt="". - Lists use
<ul>/<ol>/<dl>— not<div>salad. - Heading hierarchy is sensible (no skipped levels).
- Lint + axe tests pass in CI before merge.
Common engineering anti-patterns
The patterns that pass code review and then break in production. Catch them earlier.
- The "overlay widget" — third-party JavaScript that claims to add accessibility to an existing site. It doesn't, it frequently breaks the assistive technology layer, and it has generated its own wave of lawsuits. Background: overlay vendors.
-
role="button"on a<div>— adds the role announcement without the keyboard behaviour (Enter, Space) or focus participation. Use<button>. -
aria-hidden="true"on focusable elements — creates a focus trap where keyboard users can tab onto an element the screen reader is told to ignore. Either remove the element from the tab order withtabindex="-1"or drop thearia-hidden. - Custom dropdown without keyboard handling — a
<div>-based combobox that opens on click but ignores Arrow keys, Home/End, type-ahead and Esc. The accessible component libraries survey covers the libraries that get this right out of the box. - Toast notifications that don't announce — a
transient surface rendered without
role="status"(polite) orrole="alert"(assertive). Sighted users see it; screen-reader users don't. - Generated DOM that breaks the accessibility tree
— heavy wrappers around an input (a custom select built from
twelve nested
<div>s) often hide the actual control from AT. Test what AT sees, not just what the user sees.
The toolkit + monitoring pipeline
Most teams want three things in sequence: a one-off triage scan when something looks broken, an engineering reference when they want to understand the underlying patterns, and a continuous monitoring layer once accessibility lands in the production roadmap. Our free WCAG 2.2 scanner covers the first — paste a public URL, get an axe-driven report in a new tab. The engineering coverage at the articles desk covers the second — including accessibility-debt-as-technical-debt and accessibility-incidents-SRE-postmortem analyses for teams managing accessibility at scale.
For the continuous layer, the monitoring buyer's guide is the most opinionated piece on the site. It rates axe Monitor, Siteimprove, Level Access and Qualibooth — the last of which integrates monitoring with a manual-audit hand-off, the missing piece in most automated-only stacks — against coverage breadth, false-positive rates, and how cleanly the data plugs into engineering workflows. The whole point of monitoring is to make sure the fixes you ship today don't regress next sprint.
Frequently asked engineering questions
The questions that come up in every accessibility kick-off. Mirrored in the FAQPage structured data.
- Is
aria-labelbetter than visible text labels? -
No. Visible text is almost always better — it benefits sighted
users, sighted users with cognitive disabilities, voice-control
users (who say what they see) and translation tooling. Reach for
aria-labelonly when there is no visible text to associate (icon-only buttons) or when the visible text genuinely is not the accessible name you want. - Should I use ARIA roles for everything?
-
No. The first rule of ARIA is "do not use ARIA". Native HTML
elements come with the right role, the right keyboard behaviour
and the right accessibility-tree semantics built in. Use ARIA
when (and only when) there is no native element that fits — for
example, a tab list, a tree, or a custom dialog where you cannot
use
<dialog>. - How do I test accessibility in CI?
-
Three layers, ordered by cost. (1) Lint:
eslint-plugin-jsx-a11yon every PR. (2) Unit tests:@testing-library/jest-domplusjest-axe(or@axe-core/react) on every component. (3) End-to-end:@axe-core/playwrighton representative user journeys. Add a Pa11y or Lighthouse CI gate on staging to catch the drift the lower layers miss. - Are axe-core and Lighthouse the same?
- Lighthouse uses axe-core under the hood for its accessibility audit, but it runs a curated subset of the rules and presents them inside a broader performance / SEO / best-practices report. axe-core itself is the engine — when you want the full rule set or want to extend it, you use axe-core directly. For most teams: use Lighthouse for trend tracking, axe-core for the actual gate.
- What's the minimum accessibility test setup for a React project?
-
Add
eslint-plugin-jsx-a11yto the existing ESLint config (it ships with create-react-app and Next.js by default). Add@testing-library/jest-domandjest-axeto your unit-test setup, and callexpect(await axe(container)).toHaveNoViolations()in at least one test per component. That's the floor — it catches roughly 40% of real WCAG issues for a couple of hours of setup. - Do I need to test with real screen readers?
- Yes, for any product feature an assistive-technology user will actually use. Automated tooling catches the structural failures (missing labels, contrast, role mismatches) but not the experiential ones (focus that jumps to the footer, live regions that don't announce, an "accessible" modal that traps focus inside the wrong subtree). Plan on at least one manual sweep per major release with NVDA on Windows and VoiceOver on macOS / iOS.
Next steps
Run a quick check against the free WCAG 2.2 scanner if you're triaging a specific page. Read the screen-reader testing tools survey before you wire AT-driver into CI. And if accessibility is moving from "one-off audit" to "ongoing production concern", the monitoring buyer's guide is the most concrete piece on the site for picking a vendor.