React will happily render a clickable <div>. The browser won't complain, the JSX compiles, and it looks fine on screen. But a keyboard user can't reach it, a screen reader won't announce it as a control, and axe-core flags it the moment you test. React gives you components; it does not give you accessibility.
The data says React developers are doing better than most, though. In 2026, home pages built with React averaged 43.5 detectable accessibility errors, 22.5% below the all-site average of 56.1 (WebAIM Million, 2026). Better than Vue at 64.6 or AngularJS at 76.6, but 43.5 is not zero. This guide shows you how to close that gap with a layered testing setup: static linting, component tests, end-to-end scans, and the manual checks automation can't replace.
TL;DR: Test React accessibility in four layers. Lint JSX with
eslint-plugin-jsx-a11y(43 rules), test components withjest-axeand React Testing Library, scan full pages with@axe-core/playwright, then check focus and flow by hand. Skip@axe-core/reactfor dev-time runtime checks: it doesn't support React 18+. Automated tools catch about 57% of issues by volume (Deque, 2021), so the manual layer still matters.
Why Does React Need Its Own Accessibility Testing?
Because the framework hides the primitives that accessibility depends on. React pages averaged 43.5 errors in 2026, better than the 56.1 average but well short of clean (WebAIM Million, 2026). Component abstraction makes it easy to ship a <div onClick> that looks like a button and behaves like nothing.
Here's the useful part: every one of the six most common failures on the web is something you control directly in JSX. Low-contrast text lives in your CSS-in-JS or Tailwind classes. Missing alt, missing <label>, empty links, empty buttons, and a missing lang attribute are all props and elements you write by hand. Those six account for 96% of all detected errors (WebAIM Million, 2026), and axe-core detects every one. Our guide to fixing the 10 most common WCAG violations covers the fixes; this post covers how to catch them automatically in a React codebase.
Citation capsule: In 2026, home pages built with React averaged 43.5 detectable accessibility errors, 22.5% below the all-site average of 56.1, and better than Vue.js (64.6) or AngularJS (76.6) (WebAIM Million, 2026). React's component model reduces some errors but does not prevent the six most common failures, all of which are authored directly in JSX and detected by axe-core.
The React Accessibility Testing Stack
No single tool covers React accessibility, because the failures happen at different stages. A linter reads your JSX before it runs; axe-core inspects the rendered DOM after it does. You want both, plus a manual layer for what neither can judge. Here is the stack and what each layer catches, with current versions as of mid-2026.
Layer | Tool | Version | Catches | Misses |
|---|---|---|---|---|
Static lint |
| 6.10.2 | Authoring mistakes in JSX (missing | Anything computed at render (contrast, focus) |
Component test |
| 10.0.0 / 16.3.2 | Rendered-DOM violations per component | Cross-page flow, real navigation |
End-to-end |
| 4.12.1 | Full-page violations in a real browser | Subjective quality, keyboard journeys |
Manual | Keyboard + screen reader | — | Focus order, route-change focus, alt quality | Nothing automatable replaces it |
Notice the pattern: each tool's blind spot is the next tool's job. That's why "we run axe in CI" isn't a complete answer. Let's set up each layer.
Step 1: Lint Your JSX with eslint-plugin-jsx-a11y
Start here because linting is the cheapest feedback loop: errors surface in your editor as you type, before the code even runs. eslint-plugin-jsx-a11y 6.10.2 ships 43 rules that read your JSX syntax tree and flag accessibility mistakes at authoring time (jsx-eslint, 2025). Add it to your flat config:
// eslint.config.js
import jsxA11y from 'eslint-plugin-jsx-a11y';
export default [
jsxA11y.flatConfigs.recommended,
// ...the rest of your config
];Now a <div onClick> with no keyboard handler, an <img> with no alt prop, or an invalid aria-* attribute fails the lint step. Because it's static analysis, it catches the authoring mistake instantly, but it can't see anything that depends on rendered output, such as colour contrast or actual focus order. That's the next two layers' job.
What we see: If you're on Next.js, don't assume you already have this. The
next lintcommand was removed in Next.js 16, and the currenteslint-config-nextbundles the React and React Hooks plugins but notjsx-a11y(Next.js docs, 2025). It shipped in the legacy config through Next.js 15, which is where the "Next gives you a11y linting for free" belief comes from. On 16 and up, addeslint-plugin-jsx-a11yyourself.
Step 2: Test Components with jest-axe and React Testing Library
Linting catches syntax; component tests catch the rendered result. jest-axe 10.0.0 runs the axe-core engine against a component's actual DOM output and fails the test on any violation (npm, 2025). Pair it with React Testing Library to render the component first:
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import 'jest-axe/extend-expect';
import { SignupForm } from './SignupForm';
test('SignupForm has no axe violations', async () => {
const { container } = render(<SignupForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});When this fails, axe prints the rule ID, the offending node, and a link to the fix, so the test tells you exactly what broke. Run it per component and you get accessibility coverage that travels with the component, not bolted on at the end. On Vitest, the same pattern works: either wire axe-core's matcher into expect.extend, or use the community vitest-axe package, though that one has seen little maintenance since 2022, so calling axe-core directly is the safer bet.
Step 3: Scan Full Pages with Playwright and axe-core
Component tests miss what only appears when a page assembles: a duplicate landmark, a heading order broken across components, a modal that traps focus wrong. @axe-core/playwright 4.12.1 scans the whole rendered page in a real browser. The assertion is one line: run the scan, expect the violations array to be empty.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('home page has no WCAG A/AA violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});The withTags call scopes axe to the WCAG 2.0/2.1 A and AA rule sets so you don't get noise from best-practice rules. This is the layer you wire into continuous integration, where it gates every pull request. Our GitHub Actions setup guide walks through the full workflow, including caching and a threshold ratchet for legacy debt. For how the underlying engines differ, see axe vs Lighthouse vs WAVE vs pa11y.
Step 4: Runtime Checks in the Browser (and the @axe-core/react Trap)
For live feedback while you develop, you want axe running against your app in the browser and logging violations to the console. The obvious package for this, @axe-core/react, is the one to avoid. Its own README states it does not support React 18 and above, and the older react-axe repo it replaced is archived and deprecated (Deque, 2025). The version number still moves only because the axe monorepo bumps every package in lockstep, not because it gained React 18 support.
So what should a modern React developer use for dev-time runtime checks? Two good options: the axe DevTools browser extension, which scans the running page on demand, or Accented, a maintained community tool that markets itself as a direct replacement for @axe-core/react (Accented, 2025). Either gives you the in-browser loop without depending on an abandoned package.
The Most Common React Accessibility Mistakes (and What Catches Them)
Which tool catches which mistake matters, because a scan that passes doesn't mean the page is accessible. The table below maps the mistakes React developers make most often to the tool that actually catches each. Note the first row: a clickable <div> is caught by ESLint, not by axe, because axe can't reliably tell that a <div> was meant to be interactive.
React mistake | axe rule | ESLint (jsx-a11y) rule | WCAG SC | Caught by |
|---|---|---|---|---|
Clickable | — |
| 2.1.1 (A) | ESLint only |
|
|
| 1.1.1 (A) | Both |
Input with no associated label |
|
| 1.3.1 / 4.1.2 (A) | Both |
Icon-only |
|
| 4.1.2 (A) | axe (lint rule off by default) |
Empty or "click here" link |
|
| 2.4.4 (A) | Both |
Invalid or mismatched ARIA |
|
| 4.1.2 (A) | Both |
Missing |
|
| 3.1.1 (A) | Both |
The fixes are React-shaped versions of the same primitives. A clickable div becomes a real button, which gets keyboard support and the correct role for free:
// ❌ ESLint flags: interactive handler on a non-interactive element
<div onClick={handleDelete}>Delete</div>
// ✅ A real button: focusable, keyboard-operable, announced as a button
<button type="button" onClick={handleDelete}>Delete</button>An unlabelled input gets a <label> tied by htmlFor, which is React's spelling of the for attribute:
// ❌ no programmatic label
<input value={email} onChange={onChange} />
// ✅ htmlFor matches the input id
<label htmlFor="email">Email address</label>
<input id="email" value={email} onChange={onChange} />What React Accessibility Testing Can't Catch
Automated tools catch about 57% of accessibility issues by volume (Deque, 2021), and only about 13% of the 55 WCAG 2.2 AA success criteria with high accuracy (Accessible.org, 2025). The rest is on you and a keyboard. In a React SPA, the biggest gap is focus management: when a client-side route change swaps the page, focus stays where it was, so screen reader users don't know anything happened. No axe rule catches that; you test it by tabbing through the app yourself.
The same goes for alt text quality. A scan confirms an image has an alt attribute, not that the text is any good. alt="image" passes axe and fails a human. This is also why accessibility overlays don't fix React apps: they automate the layer that's already automatable and skip the judgment part. We cover the evidence in why accessibility overlays fail. For the compliance side of what you're testing against, the WCAG 2.2 compliance checklist maps the criteria.
What we see: Route-change focus is the single most common thing that passes a full axe scan and still fails a real screen reader user in a React app. Testing it takes ten seconds: navigate, then check where focus landed. It should move to the new page's heading or main region, not sit on the link you just clicked.
Frequently Asked Questions
Is React accessible by default?
No. React renders whatever JSX you write, including inaccessible patterns like a clickable <div>. React pages still averaged 43.5 accessibility errors per home page in 2026, better than the 56.1 average but far from zero (WebAIM Million, 2026). Accessibility comes from how you use React, not from React itself.
What is the best tool for React accessibility testing?
There isn't one; you need a layered set. Use eslint-plugin-jsx-a11y for static linting, jest-axe with React Testing Library for component tests, and @axe-core/playwright for full-page scans. Together they cover roughly 57% of issues by volume (Deque, 2021). Manual keyboard testing covers the rest.
Should I still use @axe-core/react?
No, not for React 18 or newer. Its own README states it doesn't support React 18 and above, and the predecessor react-axe is archived (Deque, 2025). Use the axe DevTools extension or a maintained alternative like Accented; both run axe-core, the engine behind the ~57% of issues automation catches by volume (Deque, 2021).
Can automated tests catch all React accessibility issues?
No. Automated tools flag only about 13% of WCAG 2.2 AA success criteria with high accuracy (Accessible.org, 2025), though they catch about 57% of real issues by volume (Deque, 2021). Focus management on route changes and alt text quality need manual testing in every React app.
The Bottom Line
React reduces some accessibility errors and introduces others, which is why the average React page still ships 43.5 of them (WebAIM Million, 2026). No single tool closes that gap. Lint your JSX with eslint-plugin-jsx-a11y, test components with jest-axe, scan pages with @axe-core/playwright, and skip the abandoned @axe-core/react for runtime checks.
Then test by hand, because the 43% automation misses (Deque, 2021) is where the worst React-specific bugs hide, especially focus on route changes. Wire the automated layers into CI with our GitHub Actions guide, read the developer's guide to web accessibility testing for the full picture, and use the common WCAG violations guide as your fix reference when a test goes red.
Sources
- WebAIM, The WebAIM Million 2026 Report, retrieved 2026-07-06, https://webaim.org/projects/million/
- Deque Systems, Automated Testing Study Identifies 57% of Digital Accessibility Issues, retrieved 2026-07-06, https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues/
- Accessible.org, Automated Scans and WCAG Success Criteria, retrieved 2026-07-06, https://accessible.org/automated-scans-wcag/
- jsx-eslint, eslint-plugin-jsx-a11y (v6.10.2), retrieved 2026-07-06, https://github.com/jsx-eslint/eslint-plugin-jsx-a11y
- Deque Systems, axe-core-npm React package README, retrieved 2026-07-06, https://github.com/dequelabs/axe-core-npm/tree/develop/packages/react
- Accented, A Modern Alternative to @axe-core/react, retrieved 2026-07-06, https://accented.dev/blog/2025-08-20-accented-a-modern-alternative-to-axe-core-react
- Next.js, ESLint Configuration Reference, retrieved 2026-07-06, https://nextjs.org/docs/app/api-reference/config/eslint