Playwright (and Cypress, and similar e2e tools) can run axe-core against your live app on every commit. The integration is straightforward; the value is catching accessibility regressions before they ship.
Why automated e2e accessibility testing matters
Unit tests check individual components; e2e tests check the whole rendered page in a real browser. Accessibility issues often emerge from the composition (header + main + modal all interacting) — exactly what e2e catches and unit tests miss.
Setting up axe-core in Playwright
Install @axe-core/playwright:
npm install --save-dev @axe-core/playwright
In your test:
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test('homepage has no critical accessibility violations', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22a', 'wcag22aa'])
.analyze()
const critical = results.violations.filter((v) => v.impact === 'critical' || v.impact === 'serious')
expect(critical).toEqual([])
})
Strategies that work
Scan critical pages, not every page
Pick 5-10 representative pages: homepage, signup, dashboard, settings, checkout. Scanning every page on every commit slows CI without finding much extra.
Block on critical and serious only
Default axe categorizes by severity: critical, serious, moderate, minor. Block deploys on critical and serious. Log moderate and minor for backlog grooming.
Scan key user flows mid-interaction
The most interesting accessibility bugs appear after interaction: modal open, form errors visible, dropdown expanded. Tell Playwright to perform the action then scan.
await page.click('button:has-text("Open dialog")')
await page.waitForSelector('[role="dialog"]')
const results = await new AxeBuilder({ page }).include('[role="dialog"]').analyze()
expect(results.violations).toEqual([])
Use disableRules sparingly
Axe has rules you may legitimately disable (e.g. color-contrast on a page intentionally low-contrast for a design demo). Document each disabled rule with a TODO.
What axe-in-Playwright catches
- Missing alt text, missing labels, missing accessible names
- ARIA validity (invalid roles, required attributes missing)
- Color contrast (computed in headless Chromium)
- Skipped headings, missing landmarks
- Empty buttons / links
What it does NOT catch (use AccessProof for these)
- Behavior over time. A scheduled scan that runs daily catches the regression introduced after the e2e suite passes.
- Multi-page consistency. CSS or JS changes that affect every page silently.
- Real network conditions. Tests run in fixed environments; AccessProof scans production exactly as users see it.
- Court-ready evidence. CI logs are not great evidence. A timestamped PDF report is.
Pair Playwright + axe (catch on every commit) with AccessProof (scheduled scans of production + PDF reports). Different layers of the same coverage strategy.
Cypress is the same idea
Use cypress-axe with the same patterns. The API differs slightly but the strategy is identical.
Originally published on access-proof.com.
Top comments (0)