Accessibility Testing Recipes

Permalink to "Accessibility Testing Recipes"

This topic collects practical, copy-ready recipes for testing the accessibility of data interfaces automatically. Each recipe addresses one layer of testing with one tool, and each is written for the specific problems data UIs have: components whose accessibility changes with state, keyboard contracts that regress silently, tables whose header wiring breaks in refactors, and dashboards with dozens of components per page.

The recipes are referenced from every section of this site. Where a guide on sortable grids, live regions or chart alternatives says “test this”, the recipe for how lives here. It is written for frontend engineers adding tests to data products and for design-system maintainers building shared test utilities.

It belongs to testing & auditing accessible data interfaces, alongside the pipeline setup in automated accessibility testing pipelines.

WCAG criteria in scope

Permalink to "WCAG criteria in scope"

Automated recipes can check parts of many criteria. This table shows which recipe covers which, and where automation stops.

Criterion Level Recipe coverage
1.3.1 Info and Relationships A axe (headers, roles), role queries (header and row structure)
4.1.2 Name, Role, Value A axe (names exist), role queries (names are right), state assertions
2.1.1 Keyboard A Keyboard contract tests in a real browser
2.1.2 No Keyboard Trap A Keyboard tests: tab in, tab out
2.4.3 Focus Order A Keyboard tests: focus after actions
4.1.3 Status Messages AA Role queries on status text; speech needs smoke tests
1.4.3 Contrast (Minimum) AA axe text contrast; charts and focus need screenshot tests
2.4.7 / 2.4.11 Focus visible / not obscured AA Screenshot-based focus tests; not axe

Prerequisites

Permalink to "Prerequisites"
The testing layers these recipes cover Layers of an accessibility test suite for data UIs, from unit-level role queries through per-state axe scans, keyboard contracts and Storybook checks, to page-level Lighthouse budgets, with screen reader smoke tests beyond automation. The testing layers these recipes coverRole queries (unit)names, roles, states — every component testPer-state axe scansstructure in sorted, empty, editing, error, dialog statesKeyboard contractsfocus movement, tab in and out, edit modeStorybook checksevery documented component state, per releaseLighthouse budgetspage-level regression net across many URLs
Each layer catches what the one above misses; screen reader tests and manual audits sit beyond all of them.

ARIA & HTML spec reference

Permalink to "ARIA & HTML spec reference"

The recipes do not introduce ARIA of their own; they assert the attributes the rest of this site recommends. The most-asserted attributes and the recipe that checks each:

Attribute / role Asserted by Typical assertion
role="grid" + name Role queries getByRole('grid', { name: 'Open invoices' })
columnheader / rowheader Role queries, axe Headers exist; th-has-data-cells passes
aria-sort Role queries, axe One header sorted; valid placement
tabindex roving Keyboard contract Exactly one tabindex="0" in the grid
aria-selected, aria-expanded Role queries State matches the action
role="status" text Role queries, keyboard tests Message present after the action
aria-invalid + description axe, role queries Error associated with the editor

Step-by-step implementation

Permalink to "Step-by-step implementation"

Step 1 — Role queries in every component test (SC 1.3.1, 4.1.2)

Permalink to "Step 1 — Role queries in every component test (SC 1.3.1, 4.1.2)"
const grid = screen.getByRole('grid', { name: 'Open invoices' });

Step 2 — Per-state axe scans (SC 1.3.1, 4.1.2, 1.4.3)

Permalink to "Step 2 — Per-state axe scans (SC 1.3.1, 4.1.2, 1.4.3)"
await test.step('sorted', async () => { /* act, settle */ expect(await scan(page, testInfo, 'sorted')).toEqual([]); });

Step 3 — Keyboard contracts as data (SC 2.1.1, 2.1.2, 2.4.3)

Permalink to "Step 3 — Keyboard contracts as data (SC 2.1.1, 2.1.2, 2.4.3)"
for (const { from, key, to } of CONTRACT) { /* focus, press, toBeFocused */ }

Step 4 — Storybook stories per state (design systems)

Permalink to "Step 4 — Storybook stories per state (design systems)"
export const SortedByAmount = { play: async ({ canvasElement }) => { /* Enter on header */ } };

Step 5 — Page-level budget (regression net)

Permalink to "Step 5 — Page-level budget (regression net)"
assertions: { 'td-headers-attr': 'error', 'categories:accessibility': ['error', { minScore: 0.95 }] }
Where each recipe runs Flow from local development through pull request checks to nightly and release runs, showing which recipes run at each stage. Where each recipe runsLocalrole queries,Storybook panelPull requestunit tests,per-state axe,keyboard contractsNightlyStorybook testrunner, LighthouseReleasescreen readersmoke testsPeriodicmanual audit
Fast checks run on every change; slow and broad checks run less often but always before release.

Keyboard interaction contract

Permalink to "Keyboard interaction contract"

The recipes verify keyboard contracts defined elsewhere on the site. A typical grid contract, as used in the keyboard recipe:

Key Context Action Asserted by Failure indicator
Tab Before grid Enter grid at the active cell toBeFocused Focus lands elsewhere
Arrow keys Grid Move one cell toBeFocused, tabindex="0" Focus moved but tabindex did not
Home / End Grid Row start / end toBeFocused No movement
Ctrl+Home Grid First cell toBeFocused —
Enter Editable cell Open editor Textbox toHaveFocus Editor not focused
Escape Editor Cancel, return focus Cell toBeFocused, text unchanged Value committed
Tab Grid Leave grid Next control focused Trapped

Screen reader compatibility matrix

Permalink to "Screen reader compatibility matrix"

Recipes on this page do not run screen readers. The matrix shows what each can and cannot tell you about screen reader experience.

Recipe Knows the name and role Knows the announcement Knows reader quirks
Role queries ✓ computed via accname Status text only ✗
axe scans ✓ exists and valid ✗ ✗
Keyboard contracts ✗ ✗ ✗
Storybook checks ✓ ✗ ✗
Screen reader smoke tests (other topic) ✓ ✓ ✓ for the readers automated
Recipe by question Matrix mapping common testing questions about data UIs to the recipe that answers each, and the speed of that recipe. Recipe by questionQuestionRecipeSpeedDoes the grid have a nameand headers?Role queriesMillisecondsIs the sorted state validARIA?Per-state axeSecondsDo arrow keys move focuscorrectly?Keyboard contractSecondsDoes every documentedstate pass?Storybook runnerMinutesDid any page get worse?Lighthouse budgetMinutesIs the sort actually spoken?Smoke test (other topic)Minutes
Pick the fastest recipe that can answer the question.

Edge cases & failure modes

Permalink to "Edge cases & failure modes"

1. Tests that pass because the component did not render

Permalink to "1. Tests that pass because the component did not render"

Diagnosis: a failed data fetch renders an empty page; axe finds nothing wrong and Lighthouse scores higher. Fix: assert content presence (row count) before every scan.

2. Scans mid-transition

Permalink to "2. Scans mid-transition"

Diagnosis: flaky contrast or structure violations that disappear on re-run. Fix: wait for a settled signal; disable animations in test config.

3. Rewriting role queries as test ids

Permalink to "3. Rewriting role queries as test ids"

Diagnosis: a refactor removes a name; the test is “fixed” with getByTestId. Fix: code review rule — changes from role to test id queries need justification.

4. Global rule disabling

Permalink to "4. Global rule disabling"

Diagnosis: a shared config turns off color-contrast to fix three charts. Fix: suppression ledger with selectors, reasons and expiry.

5. Keyboard tests that click

Permalink to "5. Keyboard tests that click"

Diagnosis: setup clicks mask missing tabindex. Fix: one programmatic focus, then keys only.

Per-state axe scans with Playwright

Permalink to "Per-state axe scans with Playwright"

axe scans of grid states with Playwright drives a grid through loaded, sorted, empty, editing, error and dialog states and scans each, with a reusable helper and settled-signal waits.

expect(await scan(page, testInfo, 'editing')).toEqual([]);

Behaviour note: the settled signals double as cheap behavioural assertions.

Keyboard contracts with Playwright

Permalink to "Keyboard contracts with Playwright"

Testing grid keyboard navigation with Playwright turns a grid’s keyboard contract into a data table of start cell, key and expected cell, asserting focus and roving tabindex.

await expect(cell(grid, ...to)).toHaveAttribute('tabindex', '0');

Behaviour note: keyboard only after the first focus — clicks hide the bugs.

cypress-axe for data tables

Permalink to "cypress-axe for data tables"

cypress-axe for data tables adds scoped scans and state commands to a Cypress suite, with violation logging that makes CI failures readable.

cy.checkA11y('#invoice-table', AXE_OPTIONS, logViolations);

Behaviour note: inject axe after every visit.

Storybook accessibility checks

Permalink to "Storybook accessibility checks"

Storybook accessibility checks for data components writes a story per state, reaches interactive states with play functions, and runs every story through axe in CI.

export const Empty = { args: { rows: [] } };

Behaviour note: the addon can only check what stories render.

Testing Library role queries

Permalink to "Testing Library role queries"

Testing Library role queries for grids and tables queries tables the way assistive technology finds things, so broken roles, names and headers fail ordinary unit tests.

within(grid).getByRole('rowheader', { name: 'INV-1042' });

Behaviour note: keep fixtures small; role queries are expensive in jsdom.

Lighthouse CI budgets

Permalink to "Lighthouse CI budgets"

Lighthouse CI accessibility budgets configures per-audit gates and a ratcheting score floor as a page-level regression net.

'button-name': 'error'

Behaviour note: a score of 100 is not WCAG conformance.

What an example suite runs per pull request Bar chart of the number of checks in an example pull request suite for a data product: role-query unit tests, per-state axe scans and keyboard contract rows. What an example suite runs per pull requestRole-query assertions (unit)420 checksPer-state axe scans36 checksKeyboard contract rows64 checks
An example suite for one product: hundreds of cheap checks per pull request, with slower suites nightly and per release.

Cross-cutting concerns

Permalink to "Cross-cutting concerns"

One configuration everywhere. axe runs inside jest-axe, Playwright, Cypress, Storybook, pa11y and Lighthouse. If each has its own rule set, results disagree and suppressions multiply. Share one options object and one suppression ledger across all of them.

Deterministic data. Accessibility tests break for the same reasons other UI tests do: random data, time-dependent content, network variance. Seed data, freeze time where content depends on it, and wait on conditions rather than timeouts.

Tests as documentation. A keyboard contract table and a story-per-state list are the most accurate documentation of a component’s accessibility behaviour. Publish them with the component.

The limits of automation. Every recipe here checks structure or keyboard focus. None checks whether announcements are timely and understandable, whether reading order makes sense, or whether a chart’s summary says the right thing. That is the job of screen reader smoke testing and manual accessibility audits.

Ownership. Tests that fail on accessibility need an owner who can fix them. Route failures to the team that owns the component, with the rule, the state and the selector in the message; a failure that says only “accessibility violations detected” is ignored.

Where to start with an existing product

Permalink to "Where to start with an existing product"

Most teams arrive here with a product that has some end-to-end tests, few accessibility checks, and more components than time. An order that pays off quickly:

  1. Switch component tests to role queries. Replace getByTestId and class selectors in existing tests with getByRole and names, one component at a time. Every conversion is a small accessibility test, and the failures you hit are real bugs.
  2. Add a scoped axe scan to the most-used data component in its three most common states — loaded, sorted, empty. Fix what it finds.
  3. Write the keyboard contract for that component as a data table and test it. Keyboard regressions are the most damaging and least visible.
  4. Share the configuration. Move the axe options and a suppression ledger into a shared module before a second team copies the first team’s setup with its own changes.
  5. Extend to the next component and to more states, guided by where users and audits report problems.
  6. Add a page-level budget once the component tests exist, as a safety net rather than the main check.

Each step leaves the product measurably better and the suite a little larger; none requires stopping feature work.

Who these recipes serve

Permalink to "Who these recipes serve"

The immediate audience is engineers, but the beneficiaries are the users whose experience the tests protect. Role queries and axe scans protect screen reader users from unnamed controls and broken tables. Keyboard contracts protect keyboard-only and switch users from traps and lost focus. Focus and contrast checks protect low-vision users. Tests are how the accessibility work done in design and implementation survives the next refactor — without them, a product’s accessibility degrades a little with every release until an audit resets it.

Design system integration

Permalink to "Design system integration"
Shared utility What it provides Used by
AXE_OPTIONS + suppression ledger One rule set and one exception process Every scanning recipe
scan(page, testInfo, state) Scoped, logged, suppression-aware Playwright scans Per-state scans
Cypress state commands + logger Same for Cypress suites cypress-axe recipe
Keyboard contract exports Per-component contract tables Keyboard recipe, product tests
Story-per-state template Required stories for data components Storybook recipe
lighthouserc preset Data-UI audit gates Lighthouse recipe

Testing checklist

Permalink to "Testing checklist"

Automated

Permalink to "Automated"

Process

Permalink to "Process"

FAQ

Permalink to "FAQ"
What should an automated accessibility test suite for a data UI include?

Role-based queries in unit tests, axe scans of each component state, keyboard contract tests in a real browser, per-story checks for design-system components and a page-level regression budget — plus screen reader smoke tests and periodic manual audits, which automation cannot replace.

Why scan components in several states?

Because much of a data component’s markup — sort state, editors, errors, empty states, dialogs — only exists after interaction. A scan after load cannot see it.

Can automated tests confirm that screen readers announce things correctly?

Not these recipes. They can check that a status message exists in the DOM; checking what a screen reader actually says needs virtual or real screen reader smoke tests.

Which recipe should a team adopt first?

Role queries in existing component tests. They need no new tools, run in milliseconds, and immediately turn missing names, roles and headers into failing tests. Add per-state axe scans and keyboard contracts for the most-used data component next.

How long do these recipes add to a CI run?

Role queries add almost nothing. Per-state axe scans add a second or two per state, and keyboard contract rows a fraction of a second each. Storybook runners and Lighthouse take minutes, which is why they usually run nightly rather than per pull request.

Should accessibility tests block merges?

Structural and keyboard tests for components should, because their failures are real regressions. Page-level budgets can start as warnings while a baseline is established, then become blocking once the known issues are fixed.

How do I stop different tools reporting different accessibility results?

Use one axe configuration and one suppression ledger across jest-axe, Playwright, Cypress, Storybook and Lighthouse, so every tool applies the same rules and exceptions.

Permalink to "Related"

← Back to Testing & Auditing Accessible Data Interfaces