React

Accessibility tests with axe: what it finds and what it misses

axe checks a React component inside the test: it catches missing roles and labels, but it cannot judge contrast, tab order or screen reader output.

Key points

  • axe runs rules against rendered DOM and returns violations with an impact level.
  • In jsdom the contrast rule never runs: no CSS, no layout, no pixels.
  • The test fails on critical impact only; lighter violations go to the backlog, not to CI.

axe catches the structural DOM failures in a test: missing roles, missing labels, a broken heading order. It cannot catch contrast, tab order or reading flow.

The base rule

axe-core, published by Deque under the MPL-2.0 licence, runs a rule set against rendered DOM. Every violation carries an impact: critical, serious, moderate or minor.

The test that works as a model

import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";

expect.extend(toHaveNoViolations);

test("il dialog non ha violazioni critiche", async () => {
  const { container } = render();
  const { violations } = await axe(container);
  expect(violations.filter((v) => v.impact === "critical")).toHaveLength(0);
});

The test renders, runs axe and fails on critical only. Filtering by impact keeps CI green: a test that fails over a deprecated aria-* attribute gets switched off within a week.

What it really checks

  • Roles and labels: an input without a label or a button without an accessible name is reported.
  • Structure: skipped headings, duplicate landmarks, lists with invalid children.
  • ARIA: unknown attributes, or aria-* that needs a role the element does not have.
  • Content: links with empty text, images without alt, iframes without a title.
  • Duplicates: two identical id values on the same page.

Installation

npm i -D vitest jsdom @testing-library/react jest-axe
npx vitest run --environment jsdom

jest-axe is MIT and axe-core is MPL-2.0: both run locally, with no external service and no network call.

Live records in the catalogue

Three records worth a first test: dialog, navigation-menu and dropdown-menu. To browse by category start from 111,985 components.

The limitation

jsdom applies no CSS, so the color-contrast rule cannot run and axe skips it without a word. An automated test covers part of WCAG, not all of it: contrast, visible focus and screen reader output stay manual.

Where to continue

The manual checklist is in accessible React components; the modal case is in accessible dialog; the test model is in Testing Library: the five tests; the measured library comparison is in React libraries 2026.

Frequently asked questions

Does axe replace a manual test?

No. It covers part of WCAG and leaves out contrast, focus and reading, so a manual test is still mandatory.

Why is contrast never checked?

Because jsdom applies no CSS. With no computed colours and no layout the rule has nothing to measure, so it is skipped.

jest-axe or vitest-axe?

They are the same engine. jest-axe works with Vitest after expect.extend, so you need no different package.

How many violations may a component have?

Zero critical. Serious and moderate violations deserve a fix, but they do not block the pipeline.

Is axe-core free for commercial use?

Yes. axe-core ships under MPL-2.0 and jest-axe under MIT: commercial use is allowed, but changes to the engine must be published.