Web Accessibility Testing with Playwright and Axe
#webdev
#a11y
#playwright
#testing
#automation
#axe
Introduction
Web accessibility is essential for building inclusive applications. By combining Playwright’s powerful browser automation with axe-core’s accessibility rules, you can automatically detect and act on accessibility violations as part of your end-to-end tests. This approach helps catch issues early, before they reach production, and fits naturally into modern CI workflows.
Why Axe with Playwright
- Axe-core provides a broad, well-maintained set of accessibility rules.
- Playwright can drive real user scenarios across multiple browsers and devices.
- Injecting axe-core into the page lets you evaluate the DOM in its actual rendering context.
- Violations can be surfaced as test failures, providing actionable feedback to developers.
Quick-start setup
-
Initialize the project and install dependencies
- npm init -y
- npm i -D playwright @playwright/test
- npm i -D axe-core
-
Basic Playwright configuration (optional)
- Create a simple Playwright test file or extend your existing tests.
-
Add Axe-core to every test you want to run
- You can load axe-core from a CDN to keep setup lightweight, or vendor it locally.
Code outline (example setup)
// Example: a11y test using Playwright with axe-core injected from CDN
import { test, expect } from '@playwright/test';
test('homepage has no a11y violations', async ({ page }) => {
await page.goto('https://example.com');
// Inject Axe core
await page.addScriptTag({ url: 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.2/axe.min.js' });
// Run axe in the page context
const results = await page.evaluate(async () => {
// @ts-ignore
return await axe.run();
});
// Log violations for debugging
if (results.violations.length > 0) {
console.error('Accessibility violations:', results.violations.map(v => {
return `${v.id}: ${v.description} (impact: ${v.impact})`;
}).join('
'));
}
// Fail the test if violations exist
expect(results.violations.length).toBe(0);
});
Tips:
- Pin axe-core versions to avoid breaking changes.
- Run a11y tests on critical pages and user flows, not just the homepage.
- Consider excluding non-app content (iframes, external widgets) when framing expectations.
Basic integration example
- Page navigation and evaluation can be part of a larger test suite. For example, run axe on frequently visited pages after performing common actions (log in, navigate to dashboards, etc.).
- You can wrap the axe.run call into a helper to reuse across tests.
Example snippet for reusability
// a11yHelper.ts
export async function runAxeOnPage(page) {
await page.addScriptTag({ url: 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.2/axe.min.js' });
// @ts-ignore
return await page.evaluate(async () => axe.run());
}
Usage in a test
import { test, expect } from '@playwright/test';
import { runAxeOnPage } from './a11yHelper';
test('dashboard is accessible after loading', async ({ page }) => {
await page.goto('https://example.com/dashboard');
const results = await runAxeOnPage(page);
if (results.violations.length) {
console.error('Violations:
', results.violations.map(v => `${v.id} - ${v.description}`).join('
'));
}
expect(results.violations.length).toBe(0);
});
Running on CI and reporting
- Integrate a11y tests into your CI pipeline alongside functional tests.
- Use the test runner’s output to fail builds when there are violations.
- Generate machine-readable reports (JSON) from axe results for dashboards:
- Save results.violations to a JSON file in your test teardown.
- Archive or publish the report as part of your CI artifacts.
Sample approach to write JSON output
const fs = require('fs');
...
const results = await runAxeOnPage(page);
fs.writeFileSync('a11y-report.json', JSON.stringify(results, null, 2), 'utf-8');
expect(results.violations.length).toBe(0);
Best practices and tips
- Scope: Focus on high-impact pages first (home, login, core workflows).
- Automation: Run a11y checks on the critical user paths, not every page in every test run.
- Performance: Axe-core is lightweight, but avoid injecting it on every single tiny page change in extremely large tests.
- Custom rules: Consider enabling runOnly rules if you need to target specific accessibility concerns.
- Accessibility fixes: Use the violations as guidance for concrete fixes, not just as pass/fail signals.
Conclusion
Pairing Playwright’s automated browser interactions with axe-core’s comprehensive accessibility checks gives you a practical, scalable way to maintain accessible software. By integrating these checks into your standard test suite and CI, you can catch and fix issues early, helping everyone use your product more effectively.