This recipe shows the smallest useful accessibility audit against QA Shop with Playwright and @axe-core/playwright.
It runs axe-core against the home page and a representative PDP, filters violations to the serious and critical impact bands, and uses expect.soft to surface every offender across both URLs in a single failure report. Catching keyboard traps, missing form labels, and color-contrast regressions in the merge gate is far cheaper than discovering them in a manual VPAT audit.
Wiring axe-core into Playwright is the right tool when you want a deterministic, CI-friendly accessibility check without standing up a separate Pa11y or Lighthouse pipeline.
§ 01 · PREREQUISITES
Prerequisites
You need a Playwright project pointing at QA Shop and the axe-core wrapper installed alongside it.
Bootstrap with npm init playwright@latest if you don't already have a project.
Run npm install --save-dev @axe-core/playwright. The package depends on axe-core itself, which npm pulls in automatically.
Set to http://localhost:3002 for a local checkout, or https://automationtestingplatform.com for the hosted demo.
§ 02 · THE TEST
The full spec
Save this file as tests/recipes/playwright/accessibility-axe.spec.ts and run it with npx playwright test --grep accessibility-axe.
// tests/recipes/playwright/accessibility-axe.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
const URLS = ["/", "/products/wooden-train-set-100-piece"] as const;
for (const url of URLS) {
test(`no serious or critical a11y violations on ${url}`, async ({ page }) => {
await page.goto(url);
if (url.startsWith("/products/")) {
await expect(page.getByTestId("page-pdp")).toBeVisible();
}
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
const blocking = results.violations.filter(
(v) => v.impact === "serious" || v.impact === "critical",
);
// expect.soft surfaces all violations in the report on a single failure,
// rather than throwing on the first one. JSON.stringify produces a
// human-readable diff in the test output.
expect.soft(blocking, JSON.stringify(blocking, null, 2)).toEqual([]);
});
}Three details deserve a callout.
The URLS tuple drives a for loop that emits one Playwright test per URL. Each test runs in its own worker context, so a critical violation on the PDP does not abort the home-page audit — both reports come back even when one fails1.
The withTags(['wcag2a', 'wcag2aa']) call scopes the audit to the WCAG 2.0 conformance bands. Without it, AxeBuilder runs every rule including best-practice hints that aren't WCAG failures, which tends to drown reviewers in noise they end up suppressing wholesale.
The assertion uses expect.soft rather than expect. A hard expect throws on the first failing assertion in a test, so a serious violation on / would mask a critical one on the PDP. expect.soft records the failure and lets the test continue, surfacing every URL's offenders in one CI report.
§ 03 · FAQ
FAQ
Common questions when extending this pattern to a real codebase.
The serialized JSON in the assertion message lists each violation's id, impact, description, and nodes[] array. Each node carries html (the offending markup) and target (a CSS selector you can paste into devtools to find it). Playwright shows this verbatim in the terminal failure block and in the HTML report under the failed assertion.
The recipes setup project (tests/recipes/playwright/setup) seeds a logged-in storageState. Add test.use({ storageState: 'playwright/.auth/recipe-verifier.json' }) at the top of the spec to audit /account, /checkout, and any other gated route under the same axe gate.
Extend the tag list to ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] once the baseline is clean. WCAG 2.1 adds rules for orientation, reflow, target size, and motion — high-value but more likely to flag legitimate design tradeoffs that need a triage pass before they go in the merge gate.
Related
For the cart-and-checkout starter test that exercises real network traffic end-to-end, see the add-to-cart Playwright recipe. For the broader Playwright vs Selenium vs Cypress picture, see the QA Shop testing guide.
The Selenium, Cypress, and WebdriverIO accessibility-audit counterparts are coming soon — placeholder slugs are /recipes/selenium/accessibility-axe, /recipes/cypress/accessibility-axe, and /recipes/webdriverio/accessibility-axe.
Footnotes
-
@axe-core/playwright — https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright (verified 2026-05-03, axe-core 4.11) ↩
Frequently asked questions
Last verified: