jest-axe in CI: Automated Accessibility Checks | Mironsoft
AI generated
{ }
React 19 · Testing · Accessibility
jest-axe in the CI Pipeline
catching accessibility violations automatically on every pull request

jest-axe wires the rule engine of axe-core directly into Jest tests and checks rendered React components against recognized WCAG rules. In a CI pipeline, this stops obvious accessibility violations from silently reaching the main branch, but it does not replace manual review.

14 min read jest-axe axe-core CI/CD

1. Why automated a11y checks in CI make sense

Accessibility problems often creep in gradually: a missing alt attribute, a form field without an associated label, or a contrast ratio that drops below the WCAG threshold after a color change in the design system. Such issues are frequently missed in a typical code review, because reviewers mostly focus on logic, readability, and functionality, not on walking through every component with a screen reader or manually measuring contrast values.

This is exactly where jest-axe comes in: it wires the established rule engine of axe-core into existing Jest tests and automatically checks rendered components against a large number of recognized WCAG rules. When this check runs as part of the CI pipeline on every pull request, a broad, if coarse, set of accessibility violations gets caught automatically before code is even merged. This shifts responsibility from an occasional manual check to a continuous safety net repeated for every change.

2. Installing jest-axe in an existing Jest setup

The installation consists of two packages: jest-axe itself and, if not already present, @testing-library/react to render the components under test. After installation, the bundled toHaveNoViolations matcher is registered globally in the Jest setup file, so it is available in every test file without importing it again. This matcher formats found violations into readable output directly in the test console, including affected DOM nodes and the violated rule.

A key point during setup is registering the matcher globally exactly once, instead of importing it again in every test file. Registering it multiple times does not cause errors, but it complicates maintenance and contradicts the usual pattern of other global matchers such as @testing-library/jest-dom. Consistently, the central registration belongs in the same setup file where jest-dom is also wired in.


// jest.setup.js
import "@testing-library/jest-dom";
import { toHaveNoViolations } from "jest-axe";

expect.extend(toHaveNoViolations);

// Example test: components/ContactForm.test.jsx
import { render } from "@testing-library/react";
import { axe } from "jest-axe";
import { ContactForm } from "./ContactForm";

test("ContactForm has no detectable a11y violations", async () => {
  const { container } = render(<ContactForm />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

3. Selectively enabling, disabling, and configuring rules

axe-core ships by default with a broad rule set spanning WCAG levels A, AA, and partially AAA, plus several best-practice rules that go beyond the formal WCAG requirements. Not every rule fits every project context. One example is the region rule, which requires all visible content to sit inside a landmark element. In an isolated component test without a surrounding page layout, this rule frequently triggers a false alarm, since landmark structure only emerges at the page level.

For such cases, axe() accepts an optional configuration object that lets you disable individual rules selectively. It matters to disable a rule deliberately and document it, for example with a comment explaining why the rule does not apply in the given test context. Blanket-disabling many rules just to get tests green faster undermines the actual purpose of the check and should be avoided.


test("isolated component without landmark context", async () => {
  const { container } = render(<PriceBadge amount={49.99} currency="EUR" />);
  const results = await axe(container, {
    rules: {
      // "region" is a false positive here: the component is tested in
      // isolation without a surrounding page layout, landmarks only exist there.
      region: { enabled: false },
    },
  });
  expect(results).toHaveNoViolations();
});

4. Integrating with the CI pipeline

Since jest-axe tests are ordinary Jest tests, CI integration usually requires no separate pipeline configuration and simply runs automatically as soon as the regular npm test command executes in the existing CI configuration. If an a11y test fails, the entire test run fails, and the pull request is blocked according to the usual branch protection rules until the violation is fixed. This effectively makes accessibility a hard merge requirement instead of an optional recommendation.

For teams that want to make a11y checks visible separately, for example to distinguish them from functional tests, a dedicated Jest project or a separate test tag works well, running a11y-specific tests in their own CI job and showing them individually in the pull request status. This makes it easier for reviewers to see at a glance whether a failed check concerns a functional problem or an accessibility violation.


// package.json (excerpt)
{
  "scripts": {
    "test": "jest",
    "test:a11y": "jest --testPathPattern='.*\\.a11y\\.test\\.jsx$'"
  }
}

// .github/workflows/ci.yml (excerpt)
// - name: Run accessibility tests
//   run: npm run test:a11y

5. Correctly testing complex, asynchronous components

For components that load data asynchronously, such as a list that first shows a loading state and renders the actual content only after a fetch completes, it is crucial to call axe() only after loading has finished. Calling the check too early, while the loading state is still shown, effectively tests the loading indicator instead of the actually relevant component, and potential violations in the final state stay undetected.

Combining waitFor from React Testing Library with axe() solves this cleanly: you first wait for an element that only exists in the loaded state, and only then run the a11y check. For components with multiple relevant states, such as loading, error, and success states, it is worth writing a separate axe() call for each state, since every state can produce different DOM structures and thus potentially different violations.


test("loaded product list has no a11y violations", async () => {
  const { container } = render(<ProductList />);

  // Wait for the loaded state first...
  await screen.findByRole("list");

  // ...only then check.
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

test("error state of the product list has no a11y violations", async () => {
  server.use(rest.get("/api/products", (req, res, ctx) => res(ctx.status(500))));
  const { container } = render(<ProductList />);

  await screen.findByRole("alert");

  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

6. Typical violations jest-axe reliably catches

In practice, jest-axe reliably catches mostly structural, machine-checkable violations: missing or duplicate id attributes on form labels, images without alt text, insufficient color contrast between text and background, broken ARIA attributes such as an aria-labelledby pointing to a non-existent ID, and incorrect heading level nesting, for example an h4 directly after an h1 with no intervening h2 and h3.

This category of errors is well suited to automated checking precisely because it can be derived purely from static DOM structure and associated attributes, without requiring an understanding of the actual usage context. That is exactly what makes jest-axe an efficient first filter that catches the most obvious and common mistakes before any manual review even takes place.

7. Limits of automated a11y checks

As valuable as jest-axe is, it demonstrably covers only part of the actual accessibility requirements. Studies by Deque Systems, the makers of axe-core, estimate that automated tools can realistically detect roughly 30 to 50 percent of all WCAG violations. The rest requires human judgment: is an alt text present but meaningless, like alt="image"? Does the tab order make sense in context, even if it technically violates no rule? Can a complex interaction pattern like a date picker actually be operated meaningfully with a screen reader?

No automated rule engine can answer such questions, since they require an understanding of meaning, context, and actual user experience. That is why jest-axe does not replace manual testing with real screen readers such as NVDA or VoiceOver, keyboard-only walkthroughs of complete user flows, or involving users with actual disabilities in the testing process. Automated checks are a necessary, but by far not sufficient, element of a complete accessibility strategy.

8. A realistic workflow: automated plus manual

A workable workflow deliberately combines both levels instead of pitting them against each other. jest-axe runs automatically on every pull request and catches the bulk of structural, unambiguously machine-detectable errors, so they never even reach the main branch. For new, larger features or complex interactive components such as multi-step forms, modals, or custom dropdowns, a manual review with keyboard navigation and a screen reader is additionally scheduled before the feature counts as done.

This combination distributes the effort sensibly: automated checks run for free on every change, while the more expensive manual review is applied specifically where automated tools structurally hit their limits. This keeps accessibility a continuous part of the development process, instead of being tacked on at the end of a project as an isolated check often performed under time pressure.

9. Conclusion: a necessary safety net, not a substitute for diligence

jest-axe in the CI pipeline is one of the most effective and, at the same time, easiest to set up measures for continuously safeguarding the technical baseline of accessibility in a React project. The integration requires little effort, runs automatically on every pull request, and reliably prevents the most obvious structural violations from ever getting merged.

At the same time, it is essential to clearly communicate the limits of this automation: a green jest-axe test does not mean a component is fully accessible, only that it satisfies the machine-checkable subset of the requirements. Teams that understand this and deliberately combine automated checks with periodic manual review achieve noticeably more robust and sustainable accessibility than those relying solely on green CI checkmarks.

Violation category Detectable by jest-axe Example Additionally needs
Missing alt attribute Yes img without alt Content quality review
Insufficient color contrast Yes Gray text on a light background Visual sign-off
Broken ARIA reference Yes aria-labelledby pointing to an invalid ID Manual screen reader review
Meaningful tab order Partially Logical flow despite technically correct structure Keyboard-only walkthrough
Usability of complex widgets No Date picker with a screen reader Manual testing with real tools

Mironsoft

React architecture, performance, and Magento frontend integration

React frontends that stay fast instead of slowing down with every feature?

We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.

Performance Audit

Systematically measuring and fixing re-renders, bundle size, and load times.

State Architecture

Cleanly separating context, client state, and server state instead of mixing everything.

Magento Integration

Building robust, type-safe GraphQL or REST integration with Magento.

10. Summary

jest-axe in CI: The Essentials at a Glance

Setup

Register the toHaveNoViolations matcher globally, call axe() inside tests.

CI integration

Runs as a normal Jest test, blocks merges on violations.

Detection rate

Automated tools realistically catch about 30 to 50 percent of WCAG violations.

Limit

Manual review with real screen readers remains essential.

11. FAQ: jest-axe in CI: The Essentials at a Glance

1What exactly does jest-axe check?
jest-axe wires the axe-core rule engine into Jest tests and automatically checks rendered components against a broad selection of recognized WCAG rules, such as missing alt text or insufficient contrast.
2How do I correctly register the toHaveNoViolations matcher?
The matcher is registered once, globally, in the Jest setup file via expect.extend(toHaveNoViolations), making it available in every test file without importing it again.
3Can I disable individual axe rules for specific tests?
Yes, axe() accepts a configuration object that lets you selectively disable individual rules, which is especially useful for components tested in isolation without a surrounding page context.
4When should I call axe() on asynchronously loading components?
Only after loading has finished, typically after a waitFor or findBy call that waits for an element present in the loaded state, so the actually relevant DOM state gets checked.
5Does a failed jest-axe test automatically block the pull request?
Yes, as long as the CI pipeline runs the regular test suite and branch protection rules define passing tests as a merge requirement, a failed a11y test blocks the merge just like any other failed test.
6What percentage of WCAG violations does jest-axe realistically catch?
According to Deque Systems, the makers of axe-core, automated tools realistically detect roughly 30 to 50 percent of all WCAG violations, with the rest requiring human judgment.
7Does jest-axe replace manual screen reader testing?
No, jest-axe does not replace manual testing with real screen readers such as NVDA or VoiceOver, since it cannot evaluate meaning, context, or actual user experience.
8Should I write a separate axe test for every component state?
For components with multiple relevant states such as loading, error, and success, it is worthwhile, since every state can produce different DOM structures and therefore potentially different violations.
9Does jest-axe need its own CI pipeline configuration?
In most cases no, since jest-axe tests are ordinary Jest tests and run automatically as soon as the regular test command executes in the existing CI configuration.
10Which typical violations does jest-axe catch especially reliably?
Structural violations derivable purely from DOM structure, such as missing alt attributes, duplicate IDs on form labels, insufficient color contrast, and broken ARIA references.