A site that passes every functional Cypress test can still be unusable for half its potential users. Missing alt text on a critical button locks out screen-reader users; 3:1 colour contrast on a primary call-to-action hides the page from anyone with mild visual impairment; a focus trap in a modal keeps keyboard users from escaping. cypress-axe brings the industry-standard axe-core engine into your Cypress runner so accessibility issues fail the build the same way functional bugs do. This lesson covers installation, the four ways to call cy.checkA11y, the impact-level rule of thumb, and the right way to integrate accessibility checks across an existing suite without breaking every test on day one.
Installing cypress-axe
npm install axe-core cypress-axe --save-devTwo packages — axe-core is the audit engine maintained by Deque; cypress-axe is the thin Cypress wrapper that injects it into the page and exposes the chainable commands. Pin both at the same time so axe rule sets stay in sync with the wrapper.
Wire it up once globally:
// cypress/support/e2e.ts
import "cypress-axe";That's the entire setup. Two new commands are now available globally: cy.injectAxe() and cy.checkA11y().
The basic check
Inject axe, then run the audit:
describe("Accessibility — homepage", () => {
it("has no critical accessibility violations", () => {
cy.visit("/");
cy.injectAxe();
cy.checkA11y();
});
});cy.injectAxe() adds the axe-core script to the page (it's not bundled with the app — it lives only inside the test runtime). cy.checkA11y() scans the entire DOM and fails the test on any violation it finds.
Without arguments, cy.checkA11y() runs all 90+ rules at every impact level. On a real-world app that's almost guaranteed to fail — that's not a problem with the test, it's a backlog of accessibility work the audit just made visible.
Targeting a specific region
Scan only one component instead of the whole page:
cy.checkA11y("[data-testid='navigation']");
cy.checkA11y(".product-card");Pass any CSS selector or a NodeList. Useful for component-level audits where the rest of the page has known issues you'll deal with later.
Filtering by impact level
Axe categorises violations into four severity tiers: critical, serious, moderate, minor. The pragmatic rule of thumb for an existing app: fail the build only on critical and serious; track the rest as backlog.
cy.checkA11y(null, {
includedImpacts: ["critical", "serious"],
});The first argument is the context (null for the whole page); the second is options. includedImpacts filters violations by severity — anything at moderate or minor is reported but doesn't fail the test.
Disabling specific rules
Sometimes a rule is a known false positive in your app — a third-party widget that fails a contrast check you can't fix, a deliberately-styled landmark axe doesn't recognise. Disable individual rules:
cy.checkA11y(null, {
rules: {
"color-contrast": { enabled: false },
"aria-allowed-attr": { enabled: false },
},
});Use sparingly. Every disabled rule is an accessibility bug class you've stopped checking. Document why each is off in a comment so the next engineer knows which rules they can re-enable when the third-party widget is replaced.
Logging violations without failing the test
When you're starting out and don't want every test to go red on day one, pass a custom callback and skip the failure:
function logViolations(violations: Result[]) {
violations.forEach((v) => {
cy.task("log",
`[a11y ${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} occurrences)`,
);
});
}
cy.checkA11y(null, null, logViolations, /* skipFailures */ true);Now violations show up in the test log without failing the build. This is the right starting point for retrofitting accessibility into a pre-existing suite — surface the issues, triage, fix the criticals, then ratchet skipFailures back to false once the floor is reached.
Common axe rules and what they catch
The full rule list is over ninety entries — these are the ones you'll see most in a real audit:
Common axe violations by typical impact level
The numbers are illustrative, not authoritative — they represent how often each rule fires across a sample of real audits. The pattern that matters: contrast, alt text, and form labels are the top three; fixing those eliminates most of the practical accessibility debt in any web app.
The companion WCAG essentials lesson covers the WCAG criteria these rules map to in human terms.
A typed checkPageA11y helper
A small helper used in afterEach runs an audit across every test in the suite without requiring each spec to opt in:
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
checkPageA11y(): Chainable<void>;
}
}
}
Cypress.Commands.add("checkPageA11y", () => {
cy.injectAxe();
cy.checkA11y(null, {
includedImpacts: ["critical", "serious"],
});
});
export {};afterEach(() => {
cy.checkPageA11y();
});Every test that doesn't already fail now also asserts the page has no critical or serious accessibility violations. Tests that do fail skip the check (so a functional failure isn't masked by an a11y failure on a half-rendered page). This is the lowest-overhead way to bring accessibility into a Cypress suite — one afterEach covers every test in every spec.
Where to put the checks
Three patterns based on suite maturity:
- New project, no debt — call
cy.checkPageA11y()inafterEach. Every test asserts accessibility from day one. Zero-tolerance. - Existing project with debt — start with one dedicated accessibility spec covering the critical pages (homepage, sign-in, checkout). Add
cy.checkPageA11y()inafterEachonly after the criticals are fixed. - Mixed environment — add accessibility checks to new specs as they're written, retrofit existing ones gradually. Track violations in your bug tracker the same way you'd track functional bugs.
There's no right answer for every team — but doing nothing is the wrong one. Even one accessibility test on the homepage catches more issues than zero accessibility tests anywhere.
A real cross-page accessibility spec
A spec that audits every key page of an e-commerce app:
const pages = [
{ path: "/", name: "Homepage" },
{ path: "/products", name: "Product list" },
{ path: "/products/headphones-1", name: "Product detail" },
{ path: "/cart", name: "Cart" },
{ path: "/checkout/shipping", name: "Checkout — shipping" },
];
describe("Accessibility — site-wide audit", () => {
beforeEach(() => {
cy.sessionLogin("alice@test.com", "Sup3rS3cret!");
});
pages.forEach(({ path, name }) => {
it(`has no critical or serious violations on ${name}`, () => {
cy.visit(path);
cy.injectAxe();
cy.checkA11y(null, {
includedImpacts: ["critical", "serious"],
});
});
});
});Five generated tests, one per page. Each one fails independently if its page has issues — a contrast bug on the cart doesn't mask a label bug on checkout. Add a page to the array tomorrow; it gets audited.
⚠️ Common mistakes
- Calling
cy.checkA11y()without first callingcy.injectAxe(). axe-core isn't part of the page until you inject it. Without the inject,checkA11yerrors out with "axe is not defined." Pair them in a custom command (or inbeforeEach) so you can never forget one. - Failing the build on every impact level on day one of an existing app. A real e-commerce homepage is probably going to fire 50+ violations the first time axe runs. Setting
includedImpactsto only["critical", "serious"](or starting in log-only mode) makes the audit triageable instead of a wall of red. - Disabling rules globally because one widget violates them. A blanket
"color-contrast": { enabled: false }turns off contrast checking for the entire app. Scope the disable to the specific selector viacy.checkA11y(null, { rules: { "color-contrast": { enabled: false } } })only when scanning that widget. Everywhere else, keep it on.
🎯 Practice task
Wire cypress-axe into a real audit. 25-35 minutes.
npm install axe-core cypress-axe --save-dev. Addimport "cypress-axe"tocypress/support/e2e.ts.- Pick a target — your own app, or
https://example.cypress.io, orhttps://www.saucedemo.com. SetbaseUrlaccordingly. - Create
cypress/e2e/a11y.cy.tswith one test that visits the homepage and runscy.injectAxe()+cy.checkA11y()with no options. Note the failures (most apps have several). - Add the impact filter:
cy.checkA11y(null, { includedImpacts: ["critical", "serious"] }). Re-run. The output should be smaller and more triageable. - Implement the log-only callback from the lesson. Run
npm run cy:runand inspect the log output for the violation messages. Confirm the test passes (because you setskipFailures: true). - Generate per-page tests — create an array of 4–5 pages and
forEachover them withit. Confirm each page becomes its own test entry in the runner. - Add the
cy.checkPageA11y()custom command and anafterEachto one of your existing functional specs. Run it. The functional tests still pass (or fail the same way); accessibility violations now also fail the build. - Stretch: find one rule that's firing for a known third-party widget and document it. Disable that rule only on the page that has the widget — leave it on everywhere else. Confirm the rest of the audit still flags the rule when other pages violate it.
The last lesson of chapter 7 ties the screenshot, video, and accessibility evidence together — Mochawesome reports and the CI-friendly outputs that let stakeholders see results without opening the runner.