jest-axe: an accessibility matcher for Jest, with the caveats written down
Custom Jest matcher for aXe for testing accessibility ♿️🃏
At a glance
- What is it?
- jest-axe is an MIT licensed Jest matcher for the axe-core accessibility engine, letting a test assert that a rendered component has no violations. Its README opens by warning that automated testing misses around 30 percent of access barriers and that colour contrast checks do not work in JSDOM and are turned off.
- Who is it for?
- jest-axe is a good pick for a React or Vue team that wants an accessibility regression check in CI at near zero cost, because the matcher integrates with the framework's own rendering utilities and reports violations in the same format as any other Jest assertion, so a failing test names the rule that broke; it is the wrong pick if you are looking for a guarantee of accessibility, since the README states plainly that around 30 percent of access barriers are missed by automated testing and that colour contrast checks are disabled in JSDOM, so before you rely on it you should read the JSDOM limitations and confirm your tests run under the jsdom environment it documents.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The warning at the top is the most useful part
jest-axe is a custom Jest matcher for the axe accessibility engine, and the first thing the README does is set expectations about what it can do.
The project does not guarantee that what you build is accessible. The GDS Accessibility team found that around 30 percent of access barriers are missed by automated testing, and the README links that finding.
The analogy it uses for the tools is the right one. Tools like axe are similar to code linters such as eslint or stylelint: they can find common issues but cannot guarantee that what you build works for users. A linter catches the mistakes that have a rule; it does not tell you whether the program does what the user needs. Accessibility is the same shape of problem, and the same consequence follows.
From that starting point the README lists what you also need to do: test your interface with the assistive technologies that real users use, and include disabled people in user research. That is the honest framing for a tool that fits in a CI pipeline in about ten lines.
The one concrete limitation that is named in its own section is more specific. Color contrast checks do not work in JSDOM, so they are turned off in jest-axe. That is a genuine gap, since contrast is one of the most common accessibility failures found by axe in the browser, and a team that assumes a green test means good contrast will be wrong. The reason is JSDOM's lack of layout and rendering, so a contrast check has no computed colours to compare against.
Installing it takes three packages
The install command is short and specific about what it expects:
npm install --save-dev jest jest-axe jest-environment-jsdomThe jsdom environment package is part of the install because axe needs a DOM to analyse, and Jest has to be told to use it. The package manifest confirms the default: the jest configuration in package.json sets testEnvironment to jsdom, with customExportConditions of node and node-addons so that ESM packages resolve correctly under that environment.
TypeScript users are pointed at a separate community maintained types package:
npm install --save-dev @types/jest-axeThat the types are community maintained rather than shipped in the package is worth noting. The main package is JavaScript, and its manifest says so, with main set to index.js and no types field, and the published files are only index.js and extend-expect.js. So a TypeScript project is relying on DefinitelyTyped style definitions maintained separately from the library itself, which is a small ongoing compatibility surface to be aware of when the library changes shape between majors.
The runtime dependencies are three: axe-core pinned at 4.12.1, jest-matcher-utils at 30.4.1, and lodash.merge at 4.6.2. The last one is there for the options merging described later.
The version policy is visible too. The current version is 11.0.0, with v10.0.0 from 2025-03-03 and v9.0.0 from 2024-06-07, and the engines field requires Node 22.13.0 or newer, so this is a project that keeps pace with the Jest major versions and expects a recent Node.
The matcher and the axe function, in two lines
The API surface is deliberately tiny, and the whole of it fits in an import, an extend and an assertion.
const { axe, toHaveNoViolations } = require('jest-axe')
expect.extend(toHaveNoViolations)And the assertion itself:
expect(await axe(html)).toHaveNoViolations()The axe function takes anything that outputs HTML and runs it through the engine, and toHaveNoViolations is the matcher that asserts the result is clean. Passing the raw HTML string is the simplest case, and it is the right pattern when you want to test a server-rendered string or a static fragment.
There is a second entry point for setup files. You can also require 'jest-axe/extend-expect', which calls expect.extend for you, and the README says this is especially helpful with the jest setupFilesAfterEnv configuration. That is the difference between writing the extend call in every test file and writing it once, and for a project with a hundred component tests it is the difference that matters.
The output when a test fails is the thing a developer will actually look at, and the README includes a screenshot of it. The matcher is built on jest-matcher-utils, which is the package Jest itself uses to format assertion diffs, so the failure message is a familiar list of violations rather than a raw object dump.
Framework by framework, the container matters
The README documents one recipe per framework, and the recurring theme is that what you pass to axe is a DOM container, not a component.
For React with ReactDOM, the container is document.body:
const { render } = require('react-dom')
const results = await axe(document.body)With React Testing Library the container comes from the render result:
const { container } = render(<App/>)
const results = await axe(container)And with React Portals, the README says to use the baseElement instead of the container, because a portal renders its children outside the container element, so a container-only scan would miss the portal content entirely. That is a real trap, and it is the kind of detail that a library that only documents the happy path would not mention.
The React Testing Library section also carries a version note: if you are using react testing library below 9.0.0 you should be using the cleanup method, which removes the rendered application from the DOM and ensures a clean HTML document for further testing. Without cleanup, one test's DOM leaks into the next one's axe scan and you get violations from a component that is not under test.
Vue has the same shape with two variants. With Vue Test Utils the container is wrapper.element, and with Vue Testing Library it is the container from render, with the same cleanup note for versions below 3.0.0.
Angular with Nx is the outlier because it is written in TypeScript and uses the Angular TestBed, and the container is fixture.nativeElement. The README also notes that you may need to extend Jest by importing jest-axe/extend-expect in test-setup.ts, since the Angular testing setup does not use the same global setup file as a plain Jest project.
Fake timers break axe, and the workaround is in the docs
There is a whole section devoted to a failure mode that will otherwise cost you an afternoon.
The symptom is a thrown timeout:
> thrown: "Exceeded timeout of 5000 ms for a test.
> Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."The cause is that aXe core does not work when timers are mocked. When you use jest.useFakeTimers, aXe core will time out often, causing failing tests. The recommendation is to re-enable the real timers temporarily around the axe call:
jest.useRealTimers();
const results = await axe(wrapper.element);
jest.useFakeTimers();This is a real design tension rather than a bug in either library. Fake timers exist so that time-dependent code can be tested deterministically, and axe-core uses timers internally to schedule its analysis. Swapping to real timers for the duration of the axe call is the pragmatic answer, and it is the reason the section exists at all.
The axe function also accepts options, with the same options as documented in axe-core. The options are applied with lodash.merge, which is the merge dependency in the manifest, so a partial options object is merged onto the defaults rather than replacing them.
The practical advice is to configure axe in one place, in a setup file, rather than repeating it in every test. The axe configuration section and the extend-expect entry point exist for exactly that.
How the package is put together
The repository is small and the shape tells you the whole story.
The top level has index.js, which is the package main and holds the axe function and the matcher, and extend-expect.js, which is the second entry point that performs the expect.extend call for you. The files array in package.json publishes exactly those two, so nothing else in the repository ships to npm.
The tests are in a __tests__/ directory, and the tooling is a deliberate choice. Linting runs npx @biomejs/biome check, configured through biome.json, rather than eslint and prettier, so the whole lint and format story is a single tool with a single configuration file. The test script chains them: npm run test runs the lint first and then the Jest suite, and the Jest run sets FORCE_COLOR=0 so the output is stable for a CI log. A .nvmrc pins the Node version, and the CHANGELOG.md tracks the releases.
The runtime dependencies give the shape of the implementation. axe-core is the engine, jest-matcher-utils produces the human readable failure output, and lodash.merge handles the options. There is no framework dependency at all, which is why the same matcher works with React, Vue, Angular or a plain HTML string. The framework-specific code all lives in the documentation as examples, not in the library.
The GitHub Actions badge points at a test workflow on the main branch, and the repository is not archived with the last push on 2026-10-01, so the project is actively maintained and keeping pace with the Jest and axe-core versions it depends on.
Editorial conclusion
jest-axe is a good pick for a React or Vue team that wants an accessibility regression check in CI at near zero cost, because the matcher integrates with the framework's own rendering utilities and reports violations in the same format as any other Jest assertion, so a failing test names the rule that broke; it is the wrong pick if you are looking for a guarantee of accessibility, since the README states plainly that around 30 percent of access barriers are missed by automated testing and that colour contrast checks are disabled in JSDOM, so before you rely on it you should read the JSDOM limitations and confirm your tests run under the jsdom environment it documents.
Frequently asked questions
How do I install and set up jest-axe?
Run npm install --save-dev jest jest-axe jest-environment-jsdom, then import the axe function and toHaveNoViolations, call expect.extend(toHaveNoViolations), and assert with expect(await axe(html)).toHaveNoViolations(). Requiring jest-axe/extend-expect instead performs the expect.extend call for you, which suits the setupFilesAfterEnv configuration.
Does jest-axe catch all accessibility problems?
No. The README states that the project does not guarantee that what you build is accessible, and that the GDS Accessibility team found around 30 percent of access barriers are missed by automated testing. It compares axe to linters such as eslint, which find common issues but cannot guarantee the result works for users.
Why does colour contrast checking not work in jest-axe?
Color contrast checks do not work in JSDOM, so they are turned off in jest-axe. JSDOM does not perform layout or rendering, so there are no computed colours for a contrast rule to compare against.
How do I use jest-axe with fake timers?
Axe core does not work when timers are mocked, so with jest.useFakeTimers it will time out and fail the test. The documented fix is to temporarily switch to real timers around the axe call with jest.useRealTimers() before it and jest.useFakeTimers() after it.
Which frameworks does jest-axe work with?
The README gives recipes for React with ReactDOM and React Testing Library, Vue with Vue Test Utils and Vue Testing Library, and Angular with Nx using the Angular TestBed, plus plain HTML strings. With React portals you should pass baseElement rather than container, and older testing library versions need their cleanup method called.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nickcolley-jest-axe)Was this useful?