# cypress-axe: running axe-core accessibility checks inside Cypress

> cypress-axe is a small TypeScript plug-in that gives Cypress three commands for accessibility testing with axe-core, including impact filtering, retries and a mode that logs violations without failing the build. It suits teams with a Cypress suite that wants automated accessibility checks in CI, and it is tied to the Cypress runner and to the axe-core runtime you install yourself.

**component-driven/cypress-axe** — Test accessibility with axe-core in Cypress

- Repository: https://github.com/component-driven/cypress-axe
- Stars: 641 · Forks: 89
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/component-driven-cypress-axe

## A thin bridge between Cypress and axe-core

cypress-axe exists to do one thing: test accessibility with axe-core in Cypress. The repository description is exactly that sentence, and it is worth taking the project at its word, because the value of a plug-in like this is that it does not try to be a testing framework.

The problem it solves is specific. axe-core is a standalone JavaScript engine that analyses a DOM and reports rule violations. Cypress is a browser-based test runner that can drive a real page. Neither of them knows about the other, so a team that wants automated accessibility checks in its Cypress suite has to write the plumbing itself: inject the axe runtime into the page under test, call it, collect the violations, decide which ones should fail the test, and print the ones a human needs to read.

That plumbing is exactly what this package provides, and it is small by design. The repository holds src/ for the TypeScript source, a cypress/ directory and cypress.config.js for the test harness, a test/ directory with a static site used by the end to end suite, and standard project files such as tsconfig.json, .eslintrc.json and .prettierrc. The published package contains just dist/, with dist/index.js as the entry point and dist/index.d.ts carrying the types, and the build is a plain tsc invocation.

Two details in the package metadata tell you how the project is versioned and who it has to work with. The peer dependencies are axe-core at ^3 or ^4 and Cypress from ^10 through ^16, so the engine and the runner are yours to choose and to upgrade on your own schedule. The engines field asks for Node 10 or newer.

The licence is MIT, the primary language is TypeScript, the default branch is master, and the authors are listed as Andy Van Slaars and Artem Sapegin, with the project sitting under the component-driven organisation.

## Installing the package and wiring in the commands

The installation is three steps, and the first one branches on your Cypress major version.

For Cypress v10 and above, you install the latest axe-core, cypress and cypress-axe:

```sh
npm install --save-dev axe-core cypress cypress-axe
```

For Cypress v9, the README says to install cypress-axe 0.x.x with your specific Cypress version, and gives the example of Cypress 9.6.0:

```sh
npm install --save-dev axe-core cypress@9.6.0 cypress-axe@0.14.0
```

That branch matters. The v9 line of this package is a different major version with a different command set shape, so mixing a 0.x cypress-axe into a Cypress v10 project, or the other way round, is not a supported combination.

The second step is including the commands. For Cypress v10 and above you update cypress/support/e2e.js, and for Cypress v9 and below you update cypress/support/index.js, in both cases by adding a single import:

```js
import 'cypress-axe'
```

The third step is adding a task to log the messages to the terminal when Cypress executes the spec files, with the README linking to the Cypress documentation for configuring a task. This is the step people skip, and it is the one that determines whether a failure is actionable. Without it, a failing accessibility assertion in Cypress gives you a rule identifier and a failure message. With it, the violation details are printed to the terminal where the person running the build can read them.

If you are using TypeScript, the README asks you to add the cypress-axe types to your Cypress tsconfig.json file:

```json
{
  "compilerOptions": {
    "baseUrl": "./",
    "target": "es5",
    "lib": ["esnext", "dom"],
    "types": ["cypress", "cypress-axe"]
  },
  "include": ["."]
}
```

The types array is the whole change, and it is what makes the three commands visible to your editor and your compiler.

## injectAxe, configureAxe and checkA11y have an order

The package adds three commands, and the order between them is not optional.

cy.injectAxe injects the axe-core runtime into the page under test. The README is explicit that you must run it after a call to cy.visit() and before you run the checkA11y command. You can call it in a test or in a beforeEach, as long as the visit comes first. The canonical form is short:

```js
beforeEach(() => {
  cy.visit('http://localhost:9000')
  cy.injectAxe()
})
```

The injection step exists because the axe runtime has to be present in the page at the moment the analysis runs, and the package will not load it for you behind the scenes. The reason is a practical one: the browser under test may be a different bundle from the one your Node process has, so the runtime has to be injected into the frame that is actually being asserted on.

cy.injectAxe accepts an optional argument of type InjectOptions, and that object can have a property axeCorePath of type string which allows you to specify the file from which axe-core will be injected. If it is not provided, the function tries to resolve the path to axe-core/axe.min.js using require.resolve, if that function is available, and if require.resolve is not available the default path node_modules/axe-core/axe.min.js is used. So you can point it at a specific file:

```js
beforeEach(() => {
  cy.visit('http://localhost:9000')
  cy.injectAxe({ axeCorePath: '<path-to-axe-core>' })
})
```

cy.configureAxe configures the format of the data used by aXe, and can be used to add new rules, which must be registered with the library to execute. The user specifies the format of the JSON structure passed to the callback of axe.run, which is why the options object looks like an axe configuration object with branding, reporter, checks, rules and locale fields.

cy.checkA11y runs axe against the document at the point at which it is called. That point is the reason the command exists in this form, because it means you can call it after interacting with your page and uncover accessibility issues introduced as a result of rendering in response to user actions. A modal that opens without focus management is invisible to a check that only runs at load time.

## Scoping the check and choosing which rules run

cy.checkA11y takes four optional parameters, and each one changes what the test actually asserts.

The first is context, which defines the scope of the analysis, the part of the DOM you would like to analyse. It will typically be the document or a specific selector such as a class name, an ID or a selector.

The second is options, a set of options passed into rules or checks, temporarily modifying them. The README draws the contrast explicitly: this contrasts with axe.configure, which is more permanent. The keys consist of those accepted by the axe.run options argument, a custom includedImpacts key, and retries and interval keys for retrying the check. So the same effect can be had two ways. configureAxe changes the engine's behaviour for the session, options change it for one call.

The basic usage is a single call:

```js
// Basic usage
it('Has no detectable a11y violations on load', () => {
  // Test the page at initial load
  cy.checkA11y()
})
```

And the scoped version passes a context and a run configuration:

```js
// Applying a context and run parameters
it('Has no detectable a11y violations on load (with custom parameters)', () => {
  // Test the page at initial load (with context and options)
  cy.checkA11y('.example-class', {
    runOnly: {
      type: 'tag',
      values: ['wcag2a']
    }
  })
})
```

The runOnly block with a tag of wcag2a is the standard way to limit the run to a conformance level, and it is worth understanding that this is a filter on the rule set, not a claim that the result is a certification.

The third parameter is violationCallback, which lets you define a callback that receives the violations for custom side-effects, such as adding custom output to the terminal. The README notes that this respects the includedImpacts filter and will only execute with violations that are included, which is a detail that catches people out when they expect a callback to see everything the engine found.

The fourth is skipFailures, optional and defaulting to false, which disables assertions based on violations and only logs violations to the console output. This lets you see violations while allowing your tests to pass, and the README says it should be used as a temporary measure while you address accessibility violations.

## Impact filters, retries and introducing checks to a legacy suite

Two options do most of the practical work in a real project, and they are the ones to understand before you write your first assertion.

The includedImpacts key is an array of strings that map to impact levels in violations. Specifying this array only includes violations where the impact matches one of the included values, and the possible impact values are minor, moderate, serious or critical. The README explains the intended use: filtering based on impact in combination with the skipFailures argument allows you to introduce cypress-axe into tests for a legacy application without failing in CI before you have an opportunity to address accessibility issues. The advice is to steadily move towards stricter testing as you address issues.

That is a migration strategy rather than a feature, and it is the honest way to use this package. A codebase that has never been audited will produce a violation list long enough to be useless, and a CI job that fails on all of it gets switched off. Starting with critical only, then adding serious, gives you a signal that stays meaningful.

The retries and interval keys handle a different problem. The retries key is an integer specifying how many times to retry the check if there are initial findings, and the interval key is the number of milliseconds to wait between retries, defaulting to 1000, one second. If retries is not specified, the check runs only once. The stated purpose is to account for dynamic content that may not be fully loaded when the check is first run.

This matters more than it sounds. An accessibility check that runs while a spinner is on screen and reports a violation for an unlabelled loading region is a false positive that trains people to ignore the output. The retry option is the difference between a check that is noisy and a check that is trusted, and it is the setting I would expect a team using this on a real application to set first.

There is also the terminal task from the installation step, which is what makes any of this legible when a test fails. A rule identifier in a failure message is a starting point; the violation detail printed in the terminal is the finding.

## Where cypress-axe is the wrong tool

The first limit is obvious and structural: this is a Cypress plug-in. If your browser tests run somewhere else, there is nothing here for you. The peer dependency list makes that concrete, pinning Cypress from version 10 to 16, and the installation instructions branch by major version because the v9 line of the package is a different shape.

The second limit is scope of coverage. axe-core analyses a rendered DOM against a rule set. It does not test whether your application is actually usable with a screen reader, whether your focus order makes sense, or whether a custom widget behaves like the ARIA pattern it claims. A green suite from cypress-axe means no detectable violations of the rules the engine checks, which is what the README's own example name says: Has no detectable a11y violations on load.

The third is that the analysis only sees what is on the page at the moment the command runs. That is why the checkA11y call site matters, and it is also the limitation: a flow that requires a user gesture, a permissions prompt, a native dialog or a third-party iframe is not covered unless you can drive it into that state.

The fourth is the version skew in the repository itself. The releases are v1.5.0 from 2023-09-01, v1.6.0 from 2025-01-30 and v1.7.0 from 2025-08-29, while the package.json in the repository carries version 1.2.0. The main branch is therefore ahead of, and not in step with, the published versions, which is normal for a TypeScript project whose package version is bumped at release time, but it means that reading the repository is not the same as reading what you would install.

The fifth is the Node floor. The engines field asks for Node 10 or newer while the dev dependencies pin TypeScript 5.6 and Cypress 15, and the Cypress peer range extends to version 16. In practice the installed Cypress version determines the Node version you need, and the engines field in this package is not the binding constraint.

The last point is about the project's own pace. The last push was on 2026-09-03, about six weeks before this review, and the newest tag is from August 2025. The code is being touched, and if you need an accessibility gate in CI next week, you should check that the version you pin still supports your Cypress major, because the peer range is the contract and the range shown today already excludes several versions.

## How the package is built and released

The build is a single command. The build script is tsc, the entry point published to npm is dist/index.js, and the type definitions are published alongside as dist/index.d.ts, with the files array in package.json restricted to dist so nothing else ends up in the tarball.

The test setup is more interesting than the build, because a plug-in that runs inside Cypress has to test itself in Cypress. The scripts show a local static site served with http-server on port 8080, Cypress opened interactively with cypress open, and a headless variant that runs Cypress against the chrome browser. The end to end entry point uses start-server-and-test to start that server and then run the suite against it, and the CI variant chains the same thing in headless mode. There is a lint script that runs eslint with the cache and fix flags, and the pretest hook runs lint before the tests, with a posttest hook that formats the repository with prettier over JavaScript and Markdown files.

There is also a husky configuration in the package metadata, with a pre-commit hook, and lint-staged in the dev dependencies, so the project expects formatted, lint-clean commits. The .prettierrc and .eslintrc.json files in the repository root are the configuration for that, and the eslint setup uses the tamia config.

The licence is MIT, in a file named License.md at the repository root, and the README states it directly. For a package that sits in a build pipeline rather than in a shipped artefact, that licence choice is unremarkable, which is the point: it can be used in a commercial product without a separate commercial agreement, subject to retaining the copyright and licence notice.

Two images sit in the repository root, cmd_log.png and terminal_output_example.png, which correspond to the terminal logging step in the installation. They are the clearest documentation in the project for what a violation looks like in your build output, and they are worth looking at before you decide how much effort to put into the task configuration.

The project also carries an .all-contributorsrc file, so the contributor list is maintained in the repository, and the repository is not archived.

## Conclusion

Adopt cypress-axe if you already run Cypress and want axe-core checks in the same suite, especially if you are introducing accessibility testing into an existing codebase and need the log-only mode to get the suite green first. Do not adopt it if your UI tests are not in Cypress, if you need a scanner that covers non-Cypress surfaces, or if you expect it to tell you whether your application is accessible rather than whether it violates the rules axe-core checks. Verify first that the axe-core version you install matches the peer range, that the support file you import is the one your Cypress major version uses, and that the impact level you assert on matches the severity your team treats as blocking, since the licence is MIT, the newest tag is v1.7.0 from 2025-08-29, and the last push was on 2026-09-03.

## FAQ

### How do I install cypress-axe in a Cypress project?

For Cypress v10 and above run npm install --save-dev axe-core cypress cypress-axe, for Cypress v9 install cypress-axe 0.x.x with your specific version, then add import 'cypress-axe' to cypress/support/e2e.js or cypress/support/index.js depending on your major version, and add a task to log the messages to the terminal.

### What is the difference between cy.configureAxe and the options argument of cy.checkA11y?

The options argument of cy.checkA11y temporarily modifies rules or checks for that single run, while cy.configureAxe configures the format of the data used by aXe more permanently, and can add new rules provided they are registered with the library to execute.

### How can I add accessibility checks without failing my CI pipeline?

Use the includedImpacts option to restrict findings to impact levels such as critical or serious, and the skipFailures argument, which defaults to false, to log violations without asserting on them. The README describes this as a temporary measure for a legacy application, with the intention of moving towards stricter testing as issues are fixed.

### Does cypress-axe work with TypeScript?

Yes. The README asks you to add the types to your Cypress tsconfig.json file, with the types array set to cypress and cypress-axe, so the commands are visible to the editor and the compiler.

## Sources

- [component-driven/cypress-axe on GitHub](https://github.com/component-driven/cypress-axe)
- [Issues](https://github.com/component-driven/cypress-axe/issues)
- [License: MIT](https://github.com/component-driven/cypress-axe/blob/master/LICENSE)
- [README](https://github.com/component-driven/cypress-axe/blob/master/README.md)
- [Releases](https://github.com/component-driven/cypress-axe/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/component-driven-cypress-axe
