Accessibility for React apps.
React makes it easy to ship dynamic UIs and just as easy to ship inaccessible ones. The shortlist of patterns, libraries, and audit workflows that actually catch the recurring failures.
What goes wrong in React projects.
Click handlers on <div> instead of <button>
The classic React anti-pattern: `<div onClick={...}>` looks fine but has no keyboard support, no focus state, no role for screen readers. Use `<button>` and style it down — every time.
Modal dialogs without focus management
Opening a modal must move focus into it; closing must return focus to the trigger. Most hand-rolled modals get this wrong. Use react-aria, headlessui, or radix-ui dialog primitives — they handle the focus trap correctly.
Conditional rendering without aria-live
When `{error && <ErrorMessage />}` renders, screen readers do not announce it unless wrapped in an aria-live region. Wrap notifications, toasts, and dynamic form errors with `role="status"` or `role="alert"`.
Custom form inputs that bypass labels
Component libraries that render `<input>` inside a complex tree often break `<label htmlFor={id}>` linkage. Always pass the same `id` to both label and input. Use `useId()` for unique IDs in SSR-safe components.
Lists without semantic markup
React makes it trivial to map over an array and render divs. But `<ul>` + `<li>` (or `<ol>` + `<li>`) carry semantic meaning that screen readers announce ("list, 5 items"). Keep the semantics.
Animations that ignore prefers-reduced-motion
Framer Motion, react-spring, GSAP — none of them respect prefers-reduced-motion by default. Wire it explicitly: `useReducedMotion()` from Framer Motion, or a `useMediaQuery("(prefers-reduced-motion: reduce)")` hook in custom code.
The React accessibility stack we recommend.
eslint-plugin-jsx-a11y
npm i eslint-plugin-jsx-a11yCatches static a11y issues in JSX at lint time — missing alt, invalid ARIA roles, keyboard event missing for click handlers. Already included in Create React App and Next.js eslint configs.
@axe-core/react
npm i @axe-core/reactRuns axe-core against your live React DOM in development. Logs WCAG violations to the console as you build. Strip in production.
react-aria (or react-aria-components)
npm i react-ariaAdobe-built primitives for accessible custom widgets. Modals, listboxes, comboboxes, tabs, menus — all with correct keyboard handling and ARIA. The most rigorous option in the React ecosystem.
Radix UI Primitives
npm i @radix-ui/react-*Unstyled, accessible UI primitives. Less rigorous than react-aria but more popular and easier onboarding. Use for dialog, dropdown, popover, tabs.
Headless UI
npm i @headlessui/reactTailwind-friendly accessible primitives. Good fit if you already use Tailwind. Slightly smaller surface than react-aria.
Step-by-step for a React accessibility audit.
- 1
Static analysis with eslint-plugin-jsx-a11y
Enable the recommended config. Most React projects already have it; check by running `npx eslint . --rule "jsx-a11y/anchor-is-valid: error"` against a known-bad file.
- 2
Runtime axe checks in dev
Add `@axe-core/react` to your app entry, gated to `process.env.NODE_ENV === "development"`. Watch the console as you build new components.
- 3
Manual keyboard testing per component
For every new interactive component, Tab through it. Confirm focus visibility, focus order, Escape behavior on modals, Arrow keys on listbox/menu.
- 4
External audit at build time
AccessProof scans the deployed URL in your CI pipeline. Block deploys that drop accessibility score below your threshold.
- 5
Manual screen reader pass before major releases
NVDA on Windows, VoiceOver on macOS. Walk through a representative user flow. Catches the cognitive and announcement issues automated tools miss.
Run a WCAG audit on your React site in 42 seconds.
External scan — no JS injected into your app
WCAG 2.2 + Section 508 + EN 301 549 in one pass
Court-ready PDF with element selectors
CI/CD gate — block deploys on regression
Works with React on any hosting (Vercel, Netlify, Fly, self-hosted)
Free plan — 1 site, monthly scan
React-specific questions.
Is React inherently inaccessible?
No. React itself emits standard HTML — accessibility depends on what your components emit. The pitfall is React makes it easy to bypass semantic HTML (div onClick, custom components that re-implement form controls) and easy to ship dynamic UI that needs careful focus management. Both are solvable with discipline and the right libraries.
Should I use react-aria or headlessui?
For maximum accessibility correctness and rich-component coverage (combobox, listbox, color picker), react-aria. For ease of integration with Tailwind and a smaller component surface, headlessui. Many teams pick headlessui for buttons/modals/menus and react-aria for complex widgets. Radix is a third option positioned between them.
How do I test focus order in React tests?
Vitest/Jest + @testing-library/user-event: `await userEvent.tab()` advances focus, then assert `document.activeElement` matches expected. Combine with screen.getByRole to find elements by accessibility role rather than test-id. Playwright also works for end-to-end focus testing.
Does Next.js help with accessibility?
Next.js inherits React patterns and adds a few specific concerns: Image component with required alt, automatic page title management via Metadata API (helps WCAG 2.4.2 Page Titled), and SSR which means hydration mismatches can produce a11y bugs that only appear after hydration. Server Components reduce some hydration risk. Otherwise, same React rules apply.