All guides
Framework guide · Framework

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.

Recurring issues

What goes wrong in React projects.

01

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.

02

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.

03

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"`.

04

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.

05

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.

06

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.

Recommended tooling

The React accessibility stack we recommend.

eslint-plugin-jsx-a11y

npm i eslint-plugin-jsx-a11y

Catches 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/react

Runs 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-aria

Adobe-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/react

Tailwind-friendly accessible primitives. Good fit if you already use Tailwind. Slightly smaller surface than react-aria.

Audit workflow

Step-by-step for a React accessibility audit.

  1. 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. 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. 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. 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. 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.

Scan your React app

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

FAQ

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.