cypress-axe for Data Tables
Permalink to "cypress-axe for Data Tables"cypress-axe adds two commands to Cypress: cy.injectAxe(), which loads axe-core into the page under test, and cy.checkA11y(), which runs a scan and fails the test on violations. For teams whose end-to-end suite is in Cypress, it is the quickest way to add accessibility checks to data table flows. Its defaults, though, produce unhelpful output — a failure message that says “N accessibility violations were detected” and little else — and teams often scan only the page as loaded.
This recipe scopes scans to tables, adds state commands, and logs violations in a form engineers can act on. It belongs to accessibility testing recipes.
Spec reference
Permalink to "Spec reference"cypress-axe API:
cy.injectAxe()— call after everycy.visit()(the page reload removes axe).cy.configureAxe(options)— set rules and checks.cy.checkA11y(context, options, violationCallback, skipFailures)—contextis a selector or{ include, exclude };optionsacceptsrunOnly,rules,includedImpacts; the callback receives violations for custom logging.
Table-related axe rules that matter most: th-has-data-cells, td-headers-attr, td-has-header (large tables), empty-table-header, table-duplicate-name, scrollable-region-focusable, aria-required-children for grids, and button-name for icon buttons in rows.
Criteria covered are structural: SC 1.3.1, 4.1.2, parts of 1.4.3 and 2.1.1 (focusable scroll regions).
When to use cypress-axe — and when not to
Permalink to "When to use cypress-axe — and when not to"Use it when your end-to-end tests are in Cypress and already reach the table states you care about. Adding checkA11y at those points costs a line each.
If you are starting fresh with no end-to-end suite, choose the test runner for other reasons; the Playwright recipe in axe scans of grid states with Playwright does the same job.
The misapplication to name is skipFailures: true left on after debugging. It logs violations and passes the test, so regressions accumulate silently in CI logs no one reads.
Annotated code example
Permalink to "Annotated code example"// cypress/support/e2e.js
import 'cypress-axe';
Cypress.Commands.add('sortTableBy', (label) => {
cy.findByRole('button', { name: label }).focus().type('{enter}'); // keyboard
cy.findByRole('status').should('contain.text', `Sorted by ${label}`); // settled
});
Cypress.Commands.add('filterTable', (q) => {
cy.findByRole('searchbox', { name: 'Filter invoices' }).clear().type(q);
cy.findByRole('status').should('contain.text', 'shown');
});
// cypress/support/a11y.js — readable violation output
export const AXE_OPTIONS = {
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'] },
};
export function logViolations(violations) {
cy.task('log', `${violations.length} accessibility violation(s)`);
cy.task('table', violations.map((v) => ({
rule: v.id,
impact: v.impact,
nodes: v.nodes.length,
target: v.nodes.map((n) => n.target.join(' ')).slice(0, 3).join(' | '),
help: v.help,
})));
}
// cypress/e2e/invoice-table.a11y.cy.js
import { AXE_OPTIONS, logViolations } from '../support/a11y';
describe('invoice table accessibility', () => {
beforeEach(() => {
cy.visit('/invoices');
cy.injectAxe(); // after every visit
cy.findAllByRole('row').should('have.length.greaterThan', 1);
});
it('loaded state', () => {
cy.checkA11y('#invoice-table', AXE_OPTIONS, logViolations);
});
it('sorted by Amount', () => {
cy.sortTableBy('Amount');
cy.checkA11y('#invoice-table', AXE_OPTIONS, logViolations);
});
it('filtered to no results', () => {
cy.filterTable('zzz-no-match');
cy.checkA11y({ include: ['#invoice-table', '.empty-state'] }, AXE_OPTIONS, logViolations);
});
});
// cypress.config.js — register the log/table tasks
setupNodeEvents(on) {
on('task', {
log(msg) { console.log(msg); return null; },
table(rows) { console.table(rows); return null; },
});
}
The findByRole commands come from @testing-library/cypress; they double as accessibility assertions, because a button that cannot be found by its role and name is itself a finding — see Testing Library role queries for grids.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"cypress-axe checks structure. Cypress can drive the keyboard (type('{enter}'), realPress with cypress-real-events for native key events), which the state commands above use so the path to each state is at least keyboard-reachable.
| Check | cypress-axe | Cypress keyboard | Needs elsewhere |
|---|---|---|---|
| Header wiring | ✓ | — | — |
| Sort button reachable by role | — | ✓ via findByRole | — |
| Sort activates with Enter | — | ✓ | — |
| Focus stays on the sort button | — | ✓ should('have.focus') |
— |
| Announcement spoken | — | — | Screen reader smoke test |
Integration context
Permalink to "Integration context"Use the same AXE_OPTIONS as every other scanner in the pipeline — configuring axe-core rules and handling false positives — so a rule decision made once applies everywhere. What the table rules check is explained in semantic HTML table construction.
For component-level checks in Cypress Component Testing, the same commands work on mounted components, which is often faster than full-page runs for design-system tables.
Running cypress-axe in CI
Permalink to "Running cypress-axe in CI"Run the accessibility specs in the same CI job as the rest of the Cypress suite, headless, against a build with seeded data. Two settings matter more than the rest. First, keep video and screenshotOnRunFailure on for these specs: a screenshot of the table state at the moment of failure makes violation output much easier to interpret. Second, split accessibility specs into their own folder (cypress/e2e/a11y/) so they can be run on their own when a design-system upgrade lands, without the full end-to-end suite.
Report the violation table in the job summary rather than leaving it in raw logs. A short script can read the logged JSON and write a Markdown table of rule, impact, state and target to the CI summary, which is the first thing reviewers see on the pull request.
Keep the suite fast by reusing one cy.visit per state where the app allows it, and by scoping scans; unscoped scans of large pages are the slowest part of most cypress-axe suites.
Gotchas
Permalink to "Gotchas"Forgetting to re-inject. Any cy.visit() or full reload removes axe from the page. Put injectAxe in the same beforeEach as the visit, and after any navigation inside a test.
Detached DOM. Re-rendered tables can detach the element Cypress captured; query by role again after each action.
Impact filtering. includedImpacts: ['critical', 'serious'] hides moderate issues that are still WCAG failures. Filter by tags, not impact.
Design system notes
Permalink to "Design system notes"Publish the Cypress state commands and violation logger with the design system’s testing utilities, alongside the Playwright helpers, so teams on either runner get the same scoped, state-based checks and the same readable output.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"How do I use cypress-axe on a data table?
Visit the page, call cy.injectAxe(), bring the table into the state you want to test, wait for it to settle, and call cy.checkA11y with the table’s selector as the context and your shared axe options.
Why does cypress-axe report nothing after navigation?
Because a page load removes axe from the page. Call cy.injectAxe() again after every visit or reload.
How can I make cypress-axe failures easier to read?
Pass a violation callback to checkA11y that logs each rule, impact, a few target selectors and the help text, using a Cypress task to print a table in the terminal.
Can cypress-axe check keyboard behaviour?
No. It runs axe-core, which checks structure. Use Cypress keyboard commands — or the cypress-real-events plugin for native key events — with focus assertions for keyboard behaviour.
Should I filter violations by impact?
Prefer filtering by WCAG tags. Moderate-impact violations can still be WCAG failures, and hiding them lets regressions through.
Related
Permalink to "Related"- axe scans of grid states with Playwright — the Playwright version
- Configuring axe-core rules — shared options
- Semantic HTML table construction — what the scans check