Accessibility testing with Playwright: WCAG checks in your test suite
Updated October 14, 2026 · 7 min read
Playwright drives a real browser, and axe-core is the open-source rule engine behind most automated accessibility checkers. Together they turn WCAG checks into ordinary tests: a page that gains a button without a name or a text with low contrast breaks the build before it reaches customers. This guide shows working examples with @axe-core/playwright, the options for React components and Storybook, CI tips, and the limits you should know about.
The examples follow the official Playwright accessibility testing guide and the @axe-core/playwright README. We use the same engine in our own scanner.
Install
npm install --save-dev @playwright/test @axe-core/playwright
The package version follows the axe-core version it ships. Its README notes that it does not follow semantic versioning, so pin the version and update it on purpose: a new axe release can add rules that find new problems.
A first WCAG test
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const WCAG_AA = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
test('home page has no detectable WCAG A/AA violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).withTags(WCAG_AA).analyze();
expect(results.violations).toEqual([]);
});
withTags limits the run to the rules tagged with these WCAG versions and levels. Without it, axe also runs its best-practice rules, which are useful but are not WCAG requirements.
Test the states people actually see
A page is more than its first render. Open the menu, the cart drawer or the cookie dialog first, then scan just that part with include:
test('mobile menu is accessible when open', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Menu' }).click();
const results = await new AxeBuilder({ page })
.include('#navigation-menu')
.withTags(WCAG_AA)
.analyze();
expect(results.violations).toEqual([]);
});
Good candidates on a shop: product page with options selected, cart with items, checkout with validation errors shown, the account login form. Using getByRole in your tests is a small accessibility test on its own: if Playwright cannot find the button by its role and name, a screen reader user probably cannot either.
Known issues without switching tests off
Real projects have problems you cannot fix today, for example inside a third-party widget. The README offers three tools:
const results = await new AxeBuilder({ page })
.exclude('#third-party-chat') // skip an element
.disableRules(['color-contrast']) // skip a rule (use rarely)
.withTags(WCAG_AA)
.analyze();
A better pattern from the Playwright guide is a snapshot of fingerprints: you record which rules fail on which elements, and the test fails only when something new appears.
const fingerprints = (results) =>
results.violations.map((v) => ({
rule: v.id,
targets: v.nodes.map((n) => n.target),
}));
expect(fingerprints(results)).toMatchSnapshot();
Two notes from the README: include and exclude take one selector per call (chain them for several), and calling withTags, withRules, disableRules or options again overrides the earlier call.
Fail on what matters first
If the backlog is large, start with the most severe problems. Every violation carries an impact of minor, moderate, serious or critical:
const blocking = results.violations.filter((v) =>
['serious', 'critical'].includes(v.impact),
);
expect(blocking).toEqual([]);
Attach the full result to the report so whoever fixes it sees every element:
// inside test('…', async ({ page }, testInfo) => { … })
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
The Playwright guide also shows how to wrap AxeBuilder in a test fixture, so every test gets the same tags and exclusions from one place.
React components: jest-axe and vitest-axe
For component tests that render into JSDOM, jest-axe and vitest-axe add a toHaveNoViolations matcher:
import { render } from '@testing-library/react';
import { axe } from 'vitest-axe';
import 'vitest-axe/extend-expect';
it('renders an accessible form', async () => {
const { container } = render(<SignupForm />);
expect(await axe(container)).toHaveNoViolations();
});
They are fast and catch missing labels and names early. Two limits from their READMEs: jest-axe turns off colour contrast checks because they do not work in JSDOM, and vitest-axe does not work with the happy-dom environment. Contrast, layout, zoom and focus need a real browser, so keep the Playwright tests for full pages.
Storybook
The Storybook accessibility addon runs axe-core on every story:
npx storybook add @storybook/addon-a11y
Set parameters.a11y.test to 'error' for stories that must stay clean: violations then fail the test in the Storybook UI and in CI. 'todo' shows them as warnings only, and 'off' skips the automatic check.
CI tips
- Run in the same pipeline as your other tests. The Playwright CI guide uses
npm ci,npx playwright install --with-depsandnpx playwright test, and uploads theplaywright-report/folder as an artifact. - Test a built, production-like page, not a dev server with debugging overlays.
- Pick a short list of key URLs — home, a category, a product, cart, checkout, login, a content page — instead of crawling everything on every commit.
- Keep the axe version pinned and update it in its own pull request.
- Do not hide failures with
disableRulesfor long; preferexcludefor one element or the snapshot pattern.
What automated tests cannot tell you
The axe-core README says that you can find on average 57% of WCAG issues automatically, and the Playwright guide is equally clear that automated testing cannot detect all types of WCAG violations. An empty violations array does not mean a page meets WCAG 2.2 AA. You still need a keyboard walk-through, a zoom and reflow check and a screen reader test. Our WCAG testing guide describes that part, and the WCAG 2.2 checklist marks which points need a person.
Where Pandus fits
Tests in CI protect the code you change. They do not see what happens on the live site afterwards: a theme update, a new app or plugin, a marketing banner, a product description pasted from a supplier. Pandus checks the production site: every page each week, with the same axe-core engine in a real browser, and a report a shop owner can read.
- The API starts checks and returns results as JSON, with webhooks when a check finishes or new issues appear — useful after a deployment.
- The browser extension checks a page as your browser shows it, including staging and pages behind a login.
- The free WCAG checker checks one public page without an account.
Pandus is an automated check, not a full audit, and not legal advice. A comparison of other tools is in accessibility testing tools.
Frequently asked questions
How do I test accessibility with Playwright?
Install @axe-core/playwright, open the page in a Playwright test, run new AxeBuilder({ page }).analyze() and assert that the violations array is empty. Limit the run to WCAG rules with withTags, for example wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa.
Does a passing axe test mean the page meets WCAG?
No. Automated rules find a part of WCAG problems only. Deque, the maker of axe-core, puts it at about 57% of issues on average. Keyboard flow, meaningful alternative text, focus order and understandable errors still need a person.
Can I use jest-axe or vitest-axe instead?
Yes, for components rendered in JSDOM. They are fast, but jest-axe turns off colour contrast checks because they do not work in JSDOM. Use Playwright for full pages in a real browser, where contrast, layout and focus can be checked.
How do I test Storybook components for accessibility?
Add @storybook/addon-a11y. It runs axe-core on every story; with parameters.a11y.test set to error, violations fail the test in the Storybook UI and in CI.
Should the build fail on every violation?
Start by failing on serious and critical issues and keep a snapshot of known ones, so new problems break the build while old ones are fixed step by step. Tighten the rule once the backlog is gone.
Sources
- Playwright, Accessibility testing: https://playwright.dev/docs/accessibility-testing
- Playwright, Setting up CI: https://playwright.dev/docs/ci-intro
- Deque, @axe-core/playwright README: https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md
- Deque, axe-core README: https://github.com/dequelabs/axe-core
- jest-axe README: https://github.com/NickColley/jest-axe
- vitest-axe README: https://github.com/chaance/vitest-axe
- Storybook, Accessibility tests: https://storybook.js.org/docs/writing-tests/accessibility-testing