I spent a couple of hours building a custom logging callback for cypress-axe. It formatted violations into a console table and registered Cypress tasks in the config file. It worked. Then I installed wick-a11y and got better output with zero custom code.
This is the second article in my accessibility testing series. The first one covered Cypress with cypress-axe, and you can find it here:
Sebastian Clavijo Suero (@sebastianclavijo) built wick-a11y on top of the same axe-core engine that powers cypress-axe. The difference is what happens after the scan. Where cypress-axe gives you a raw violations array and leaves the rest to you, wick-a11y processes the results into color-coded Cypress logs with visual highlighting in the runner, plus HTML reports with annotated screenshots and fix guidance. All of it works out of the box.
What wick-a11y adds over cypress-axe
I've been using cypress-axe in my project for some time, and it works well. However, there is a gap between when "axe-core detects violations" and when "the team understands what needs to be fixed." To address this, I created a terminalLog callback and implemented custom severity filtering. I've noticed that many teams do something similar to this.
The API itself shows the gap. cypress-axe uses positional arguments: cy.checkA11y(context, options, violationCallback, skipFailures). When you only need the skip flag, you end up writing cy.checkA11y(null, null, terminalLog, true). Three nulls to reach the one parameter you care about. wick-a11y uses cy.checkAccessibility(context, options) with a clean options object. Everything goes in one place.
wick-a11y closes that gap with features that come built in. It uses cy.checkAccessibility() instead of cy.checkA11y(), and here's what you get without writing any custom code:
Violations appear in the Cypress log grouped by severity, with a count summary (critical, serious, moderate, minor). Hovering over a violation in the log highlights the affected DOM element on the page with a color-coded outline. Clicking it prints full details to the browser console, including the WCAG rule and fix guidance.
HTML reports are generated automatically for every test that finds violations. Each report includes violation details grouped by severity and a screenshot highlighting the affected elements. The reports themselves are mostly WCAG 2.2 AAA compliant and support full keyboard navigation. An accessibility testing tool whose own reports are accessible is a detail Sebastian clearly thought about.
There's also a voice feature. When enabled, wick-a11y reads violation summaries aloud at the suite, test, violation type, and individual element levels. Sebastian added this because many people testing accessibility have disabilities themselves. The Cypress runner is not particularly accessible, and this gives auditory feedback for people who need it.
The official Cypress blog featured wick-a11y in their Open Source Accessibility Plugins in Cypress post, describing it as adding visual, reporting, and voice dimensions to the cypress-axe plugin.
Installation and setup
Two packages (wick-a11y pulls in cypress-axe as a dependency):
npm install --save-dev wick-a11y axe-core
Add the accessibility tasks to your cypress.config.js:
const addAccessibilityTasks = require('wick-a11y/accessibility-tasks');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
addAccessibilityTasks(on);
},
},
});
Import the custom commands in cypress/support/e2e.js:
import 'wick-a11y';
That's it. No terminalLog callback, no task registration for log and table, and no custom severity filtering code. The plugin manages everything.
HTML reports are saved to cypress/accessibility by default. You can change this:
module.exports = defineConfig({
accessibilityFolder: 'cypress/your-reports-folder',
});
Basic usage patterns
Full page scan
it('home page has no detectable violations', () => {
cy.visit('/');
cy.injectAxe();
cy.checkAccessibility();
});
When I first ran this on my weather app, the Cypress log showed a severity summary at the top: 3 critical, 1 serious, 2 moderate, 1 minor. Each violation is listed below, along with the affected elements. On the page itself, every element with a violation had a colored outline matching its severity. Red for critical, blue for minor, and the rest in between.
Scope to an element
cy.checkAccessibility('[role="dialog"]');
cy.checkAccessibility('form');
Filter by severity
cy.checkAccessibility(null, {
includedImpacts: ['critical', 'serious'],
});
Warn without failing
This is the feature I was building manually with the skipFailures flag in cypress-axe. The difference: cypress-axe's skipFailures is a boolean. Either all violations fail the test, or none do. wick-a11y has onlyWarnImpacts, which lets you pick which severity levels to warn without failing:
cy.checkAccessibility(null, {
onlyWarnImpacts: ['moderate', 'minor'],
});
This keeps the pipeline green while still surfacing moderate and minor issues in the Cypress log and HTML report. The test fails only for critical and serious violations. With cypress-axe, you'd have to build that filtering yourself.
Filter by WCAG tags
wick-a11y supports WCAG 2.2 at all three levels (A, AA, AAA):
cy.checkAccessibility(null, {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa'],
},
});
Custom severity styles
You can override the default highlight colors for each severity level:
cy.checkAccessibility(null, {
impactStyling: {
critical: { icon: '🔴', style: 'fill: #E24B4A; stroke: #E24B4A;' },
serious: { icon: '🟠', style: 'fill: #EF9F27; stroke: #EF9F27;' },
},
});
Control HTML report generation
// detailed report (default)
cy.checkAccessibility(null, { generateReport: 'detailed' });
// basic report (smaller file, no JavaScript)
cy.checkAccessibility(null, { generateReport: 'basic' });
// no report
cy.checkAccessibility(null, { generateReport: 'none' });
Example 1: E2E test with real API calls
describe('E2E: city search + accessibility', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('checks accessibility after a search', () => {
cy.intercept('/search?q=*').as('search');
cy.intercept('/weather?*').as('weather');
cy.get('[data-cy="city-selector"]').type('Toronto');
cy.wait('@search').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
});
cy.get('#city-select').select(1);
cy.wait('@weather').then((interception) => {
const cityName = interception.response.body.name;
cy.get('[data-cy="weather-display"]')
.should('be.visible')
.and('contain', cityName);
});
cy.checkAccessibility(null, {
onlyWarnImpacts: ['moderate', 'minor'],
});
});
});
After the test runs, wick-a11y generates an HTML report in cypress/accessibility that groups violations by severity. The screenshot at the bottom of the report shows my weather app with the select dropdown highlighted in red (critical: no accessible name) and several text elements outlined in orange (serious: contrast ratio). I sent that report to the team, and they fixed the dropdown label within the hour. With cypress-axe, I would have had to explain the console output. The report explained itself.
Example 2: Scoped checks for modals and dynamic content
describe('Weather app modal', () => {
beforeEach(() => {
cy.visit('http://localhost:8080/');
cy.injectAxe();
});
it('checks modal accessibility', () => {
cy.contains('button', 'Open Modal').click();
cy.checkAccessibility('[data-cy="modal-overlay"]', {
onlyWarnImpacts: ['moderate', 'minor'],
});
cy.get('[data-cy="modal-overlay"]').should('be.visible');
});
});
Hovering over a violation in the Cypress log highlights the affected element in the runner. I found a close button with no accessible name this way. In the runner, the button lit up red when I hovered over the label violation in the log. That visual connection between the log entry and the actual element on the page is something I never had with cypress-axe.
Example 3: Keyboard navigation with cy.press()
wick-a11y handles the axe-core scan. For keyboard behavior, you still need cy.press() (Cypress 14+):
describe('Modal keyboard navigation', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('traps focus inside the modal and closes on Escape', () => {
cy.contains('button', 'Open Modal').click();
cy.get('[data-cy="modal-overlay"]').should('be.visible');
cy.focused().should('have.attr', 'data-cy', 'modal-close-btn');
cy.press('Tab');
cy.focused().should('be.within', cy.get('[data-cy="modal-overlay"]'));
cy.press('Escape');
cy.get('[data-cy="modal-overlay"]').should('not.exist');
cy.focused().should('contain', 'Open Modal');
cy.checkAccessibility(null, {
onlyWarnImpacts: ['moderate', 'minor'],
});
});
});
This test evaluates four aspects that axe-core cannot check: 1) whether focus moves into the modal when it opens, 2) whether the Tab key does not allow focus to escape the modal boundary, 3) whether the Escape key closes the dialog, and 4) whether focus returns to the button that triggered the modal. All of these elements are requirements of WCAG 2.4.3 (Focus Order).
Example 4: Storybook integration
describe('Storybook: CitySelector', () => {
beforeEach(() => {
cy.visit(
'http://localhost:6006/iframe.html?id=components-cityselector--default'
);
cy.injectAxe();
});
it('has no accessibility violations', () => {
cy.checkAccessibility(null, {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa'],
},
});
});
});
Each story gets its own HTML report. I started running these for our shared component library after a dropdown component shipped to multiple applications without an accessible name. The wick-a11y reports made it easy to share findings with the team because the screenshots showed exactly which elements failed and why.
Example 5: Component tests
import CitySelector from './CitySelector';
describe('<CitySelector />', () => {
beforeEach(() => {
cy.injectAxe();
});
it('renders with no critical violations', () => {
cy.mount(<CitySelector />);
cy.checkAccessibility(null, {
includedImpacts: ['serious', 'critical'],
});
cy.get('[data-cy="city-selector"]').should('be.visible');
});
});
If the label is missing here, it will be missing everywhere the component is used. wick-a11y's HTML report for component tests includes the same annotated screenshot and fix guidance as the E2E reports, so the developer working on the component can see the problem and the suggested fix in a single document.
cypress-axe vs wick-a11y: when to use which
Both use the same axe-core engine underneath. The scan results are the same, but the differences lie elsewhere.
cypress-axe gives you the raw results and leaves the presentation to you. You write the logging callbacks, register the tasks, and build your own reporting. If you want minimal dependencies and full control, it works. But you'll write and maintain custom code that wick-a11y provides out of the box.
wick-a11y gives you color-coded Cypress logs, visual highlighting in the runner, HTML reports with annotated screenshots, and voice feedback. If your team needs to share findings with designers or product managers who don't read terminal output, the built-in reports save real time.
A note on cypress-axe maintenance
One thing worth knowing: cypress-axe caps some features that axe-core supports natively. Axe-core returns "incomplete" results for checks it can't fully confirm, such as certain color-contrast situations that require manual review. cypress-axe filters those out before they reach your test. Sebastian raised this as issue #191 in October 2025. As of this writing, it's still open with no response.
The maintenance gap goes beyond that one issue. The cypress-axe new API RFC (issue #75) has been open since November 2020 with community demand but no implementation. Most recent activity has been version bumps to keep pace with new Cypress releases. By contrast, wick-a11y has had over 14 releases in just over a year. Version 2.2.0 shipped for Cypress 15 within ten days of the Cypress release. Version 3.0.1 is the latest as of March 2026. Sebastian tracks Cypress major versions and publishes compatibility updates fast.
Sebastian's plan is to remove the cypress-axe dependency from wick-a11y and use axe-core directly. Since cypress-axe is a thin wrapper over axe-core with one additional filtering feature that wick-a11y already bypasses for its warnings system, the change should be transparent to users.
Resources
- wick-a11y on GitHub
- wick-a11y on npm
- Sebastian's introductory article on dev.to
- wick-a11y video tutorial
- Open Source Accessibility Plugins in Cypress (official Cypress blog)
- Axe Core documentation (Deque)
- WCAG 2.2 quick reference (W3C)
- Companion article: Cypress + cypress-axe version
The code examples in this article use a weather app I built for testing. My book Ultimate Web Automation Testing with Cypress covers the Cypress framework end to end.
Find me on LinkedIn.

Top comments (0)