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.
Table of Contents
- 1. Why automated a11y checks in CI make sense
- 2. Installing jest-axe in an existing Jest setup
- 3. Selectively enabling, disabling, and configuring rules
- 4. Integrating with the CI pipeline
- 5. Correctly testing complex, asynchronous components
- 6. Typical violations jest-axe reliably catches
- 7. Limits of automated a11y checks
- 8. A realistic workflow: automated plus manual
- 9. Conclusion: a necessary safety net, not a substitute for diligence
- 10. Summary
- 11. FAQ
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.