How to Test React Accessibility with axe

Run the axe engine against rendered React components with jest-axe so inaccessible markup fails the same run as every other test.

7 min read

Axe is an accessibility engine that scans rendered HTML against WCAG rules and reports violations. This guide shows how to test React accessibility with axe inside a Vitest suite using jest-axe, so inaccessible markup fails the same run as every other test.

What axe catches and misses

Axe finds around 57 percent of WCAG issues automatically, such as missing alternative text, unlabeled inputs, and click handlers on non-interactive elements. It returns zero false positives by design, but it cannot replace manual testing.

Two limits matter in a jsdom test suite. The color-contrast rule does not run in jsdom, so contrast must be checked in a real browser. And a known happy-dom bug breaks axe's DOM introspection, so the test environment must be jsdom.

Install and register the matcher

jest-axe wraps the axe engine in a matcher and works the same way under Vitest as it does under Jest, because Vitest's expect supports the same expect.extend API. Install it as a development dependency.

bashbash
npm install -D jest-axe

Register the matcher once in the test setup file so every test can use it without repeating the import. Vitest's expect.extend accepts the same matcher object jest-axe exports for Jest.

index.jsindex.js
// src/setupTests.js
import { toHaveNoViolations } from "jest-axe";
import { expect } from "vitest";
 
expect.extend(toHaveNoViolations);

Register the setup file in the test setupFiles array of your config, and make sure the environment is jsdom.

Write an accessibility test

A save button built from a div with a click handler is the classic violation axe exists to catch.

App.jsxApp.jsx
export default function SaveButton({ onClick }) {
  return <div onClick={onClick}>Save</div>;
}

The test renders the component, then runs axe against its container and asserts there are no violations. One short test covers every rule axe ships.

App.jsxApp.jsx
import { render } from "@testing-library/react";
import { axe } from "jest-axe";
import SaveButton from "./SaveButton";
 
test("has no accessibility violations", async () => {
  const { container } = render(<SaveButton />);
 
  expect(await axe(container)).toHaveNoViolations();
});

Axe flags the div because a click handler needs keyboard support, a role, and focus handling. The failing matcher prints the rule name and the offending element so the fix is obvious.

Fix the violation and re-run

Swapping the div for a native button removes the violation without extra ARIA.

App.jsxApp.jsx
export default function SaveButton({ onClick }) {
  return <button onClick={onClick}>Save</button>;
}

A button is focusable, clickable by keyboard, and exposed with the right role for free. The same test now passes, and the change made the real component accessible rather than the test easier.

Configure axe for isolated components

Axe assumes it is scanning a whole page, so it expects landmark regions. When a test renders one component in isolation, disable that rule for that test.

App.jsxApp.jsx
const results = await axe(container, {
  rules: { region: { enabled: false } },
});
expect(results).toHaveNoViolations();

The options object is the same one axe-core accepts, so individual rules can be scoped without weakening the rest of the scan. For the queries that find the elements axe checks, see how to query elements in React Testing Library.

Common mistakes

  • Disabling failing rules to make a test green instead of fixing the markup.
  • Running axe under happy-dom and blaming the tool for the failure.
  • Testing only one component and expecting axe to cover the whole page.
  • Skipping manual review because the automated run is clean.

What to learn next

jest-axe pairs with the runner setup from how to test React components with Vitest. The quality habits that keep the rest of the suite healthy are covered in the common mistakes guide.

Rune AI

Rune AI

Key Insights

  • Install jest-axe and register its matcher in the test setup.
  • Run axe against the rendered container and assert no violations.
  • Use jsdom, not happy-dom, because axe depends on isConnected.
  • Disable the region rule when testing isolated components.
  • Treat a clean run as a baseline, not proof of full accessibility.
RunePowered by Rune AI

Frequently Asked Questions

Does axe catch every accessibility problem?

No. Axe finds roughly 57 percent of WCAG issues automatically. Color contrast does not run in jsdom, and manual review with real assistive technology is still required.

Does jest-axe work with happy-dom?

No. A known happy-dom bug breaks axe's DOM introspection, so the test environment must be jsdom even when the runner is Vitest.

Conclusion

jest-axe wraps the axe engine in a matcher that fails on violations. Run it against every rendered component and keep the environment on jsdom.