Toolkit · For developers

Web accessibility for developers — built for engineers who'd rather fix the root cause than ship an overlay

The engineering-shaped slice of the site. Semantic HTML patterns, ARIA roles that actually work in production, screen-reader-aware testing harnesses, CI integration recipes, and the framework-by-framework state-of-the-art for keyboard handling.

Curated as an entry point: the patterns, the tools, the test harnesses, and the framework-specific guides engineers actually need to ship accessible features without overlay theatre. Cross-linked to the free WCAG 2.2 scanner for one-off triage and the monitoring buyer's guide for continuous coverage.

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.

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-describedby or adjacent text).
  • Dynamic content changes announce via aria-live when 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 with tabindex="-1" or drop the aria-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) or role="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-label better 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-label only 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-a11y on every PR. (2) Unit tests: @testing-library/jest-dom plus jest-axe (or @axe-core/react) on every component. (3) End-to-end: @axe-core/playwright on 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-a11y to the existing ESLint config (it ships with create-react-app and Next.js by default). Add @testing-library/jest-dom and jest-axe to your unit-test setup, and call expect(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.