Related articles: Also see which testing framework makes accessibility testing easiest, extending Playwright beyond functional checks to quality attributes, and catching accessibility violations early with shift-left practices.
Accessibility Testing with Playwright and Axe: Catch Every WCAG Violation in CI
Accessibility is not a feature�it's a fundamental right. Yet, according to WebAIM's 2025 Million Report, over 96% of the top one million home pages had detectable WCAG 2 failures. This is not because developers don't care; it's because accessibility is complex, constantly evolving, and often tested manually if at all.
The good news? Modern tools like Playwright, combined with the axe-core accessibility engine, make it possible to automate a significant portion of accessibility testing. By integrating these checks into your continuous integration pipeline, you can catch and fix violations early, before they reach production.
In this guide, we'll cover:
- What accessibility testing is and why it matters
- How Playwright and axe-core work together
- Step-by-step implementation of automated accessibility checks
- Best practices for WCAG compliance and ARIA patterns
- Common pitfalls and how to avoid them
Whether you're a QA engineer, developer, founder, or no-code tester, this article will give you the tools and knowledge to build more inclusive web experiences.
What is Web Accessibility (a11y)?
Web accessibility means ensuring that websites, applications, and digital tools are usable by everyone�including people with disabilities. This includes:
- Visual impairments: Blindness, low vision, color blindness
- Auditory impairments: Deafness or hearing loss
- Motor impairments: Difficulty using a mouse or keyboard
- Cognitive impairments: Learning disabilities, memory issues, attention disorders
The Web Content Accessibility Guidelines (WCAG), published by the W3C, are the global standard for accessibility. The most recent version is WCAG 2.2, with a forthcoming WCAG 3.0 (formerly "Silver") on the horizon. WCAG is organized around four principles, known as POUR:
| Principle | Description |
|---|---|
| Perceivable | Information must be presentable to users in ways they can perceive (e.g., text alternatives for images). |
| Operable | UI components and navigation must be operable (e.g., keyboard navigation). |
| Understandable | Information and operation of the UI must be understandable (e.g., clear labels, predictable navigation). |
| Robust | Content must be robust enough to be interpreted by assistive technologies (e.g., valid HTML, ARIA). |
WCAG has three conformance levels:
- Level A: Basic accessibility (minimum legal requirement in many jurisdictions)
- Level AA: Recommended target for most public-facing sites
- Level AAA: Enhanced accessibility (aspirational for most organizations)
Why Automate Accessibility Testing?
Manual accessibility testing�using screen readers like JAWS, NVDA, or VoiceOver�is essential, especially for complex interactions and user flows. However, it's time-consuming and requires specialized expertise.
Automated accessibility testing can:
- Catch a large percentage of common issues (estimated 30-50% of WCAG violations)
- Run continuously as part of your CI/CD pipeline
- Provide immediate feedback to developers during local development
- Scale across hundreds of pages and components with minimal effort
- Establish a baseline and prevent regressions
Tools like axe-core are highly respected in the a11y community. Developed by Deque Systems, axe-core is an open-source accessibility testing engine that runs against the DOM and reports violations based on WCAG and other standards.
Playwright + Axe: The Perfect Pairing
Playwright is a modern browser automation framework. axe-core is a powerful JavaScript library for accessibility testing. When combined, you can:
- Navigate to a page or component with Playwright
- Inject axe-core into the page
- Run an accessibility scan
- Assert that no violations exist
The community-maintained @axe-core/playwright package makes this integration seamless.
Setting Up Playwright with Axe
Prerequisites
Ensure you have a Playwright project set up. If not:
npm init playwright@latest
Installation
Install the axe-core Playwright integration:
npm install --save-dev @axe-core/playwright
Basic Usage
Here's a simple test that scans a page for accessibility violations:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage should not have accessibility violations', async ({ page }) => {
await page.goto('https://example.com');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
If any violations are found, the test will fail and output a detailed report with:
- The rule that was violated (e.g.,
color-contrast,label,image-alt) - The impact level (
minor,moderate,serious,critical) - The HTML nodes that failed
- Suggestions for how to fix the issue
Advanced Configuration
Target Specific Regions
You can limit the scan to a specific part of the page:
const results = await new AxeBuilder({ page }).include('#main-content').exclude('.advertisement').analyze();
Disable Specific Rules
Some rules may not apply to your application or may generate false positives. You can disable them:
const results = await new AxeBuilder({ page }).disableRules(['color-contrast', 'duplicate-id']).analyze();
Caution: Only disable rules when you have a valid reason and document it in your test.
Test Against Specific WCAG Levels
By default, axe tests against WCAG 2.1 Level A and AA. You can customize this:
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();
Set a Baseline and Allow Only Specific Violations
If you're retrofitting accessibility into an existing app, you might have many existing violations. You can capture a baseline and only fail on new violations:
import fs from 'fs';
test('should not introduce new a11y violations', async ({ page }) => {
await page.goto('https://example.com');
const results = await new AxeBuilder({ page }).analyze();
const baseline = JSON.parse(fs.readFileSync('a11y-baseline.json', 'utf8'));
const newViolations = results.violations.filter(
(v) => !baseline.some((b) => b.id === v.id && b.nodes.length === v.nodes.length),
);
expect(newViolations).toEqual([]);
});
Testing Common Accessibility Patterns
1. Keyboard Navigation
One of the most critical aspects of accessibility is ensuring that all interactive elements are keyboard accessible.
test('should be fully keyboard navigable', async ({ page }) => {
await page.goto('https://example.com');
// Start from the first focusable element
await page.keyboard.press('Tab');
let focusedElement = await page.evaluate(() => document.activeElement.tagName);
console.log('First focused element:', focusedElement);
// Continue tabbing through the page
for (let i = 0; i < 10; i++) {
await page.keyboard.press('Tab');
focusedElement = await page.evaluate(() => document.activeElement.tagName);
expect(['A', 'BUTTON', 'INPUT', 'TEXTAREA']).toContain(focusedElement);
}
});
2. Focus Management in Modals
When a modal opens, focus should move to the modal and be trapped within it until it closes.
test('modal should trap focus', async ({ page }) => {
await page.goto('https://example.com');
await page.click('button[aria-label="Open modal"]');
await page.waitForSelector('[role="dialog"]');
// First element inside the modal should be focused
const firstFocusable = page.locator('[role="dialog"] button').first();
await expect(firstFocusable).toBeFocused();
// Tab to the last element and ensure focus cycles back
await page.keyboard.press('Shift+Tab');
const lastFocusable = page.locator('[role="dialog"] button').last();
await expect(lastFocusable).toBeFocused();
});
3. ARIA Patterns
ARIA (Accessible Rich Internet Applications) attributes provide semantic meaning to custom components. For example, a navigation menu should have role="navigation", and buttons that control other elements should use aria-controls.
test('navigation should have correct ARIA roles', async ({ page }) => {
await page.goto('https://example.com');
const nav = page.locator('nav');
await expect(nav).toHaveAttribute('aria-label', 'Main navigation');
const menuButton = page.locator('button[aria-expanded]');
const isExpanded = await menuButton.getAttribute('aria-expanded');
expect(isExpanded).toBe('false');
await menuButton.click();
await expect(menuButton).toHaveAttribute('aria-expanded', 'true');
});
4. Screen Reader Announcements (Live Regions)
Dynamic content updates should be announced to screen readers using aria-live.
test('notifications should be announced to screen readers', async ({ page }) => {
await page.goto('https://example.com');
const liveRegion = page.locator('[aria-live="polite"]');
await expect(liveRegion).toBeEmpty();
await page.click('button#trigger-notification');
await expect(liveRegion).toHaveText('Your action was successful!');
});
Common Accessibility Violations and How to Fix Them
| Violation | Description | Fix |
|---|---|---|
| color-contrast | Text does not have sufficient contrast. | Ensure text has at least 4.5:1 contrast (7:1 for AAA). |
| image-alt | Images missing alt attributes. |
Add descriptive alt text for informative images; use alt="" for decorative images. |
| label | Form inputs missing associated labels. | Use <label for="input-id"> or aria-label attributes. |
| button-name | Buttons without accessible names. | Use text content, aria-label, or aria-labelledby. |
| link-name | Links without descriptive text. | Avoid "click here". Use descriptive link text like "Read the full report". |
| aria-required-children | ARIA roles used incorrectly. | Ensure that roles like list contain listitem children. |
| heading-order | Headings skipped (e.g., <h1> to <h3>). |
Maintain a logical heading hierarchy. |
| landmark-unique | Multiple landmarks of the same type without labels. | Add unique aria-label to each landmark (e.g., <nav aria-label="Primary">, <nav aria-label="Footer">). |
Integrating Accessibility Tests into CI/CD
To prevent regressions, run your accessibility tests on every pull request.
Example GitHub Actions Workflow (.github/workflows/a11y-tests.yml):
name: Accessibility Tests
on:
pull_request:
branches:
- main
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '22'
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:a11y
- if: failure()
run: npx playwright show-report
Create a dedicated test script in package.json:
"scripts": {
"test:a11y": "playwright test tests/a11y.spec.ts"
}
Accessibility Beyond Automation
While automated testing is incredibly valuable, it's not a complete solution. Manual testing with assistive technologies is essential to catch:
- Screen reader usability issues: Does the navigation make sense when read aloud?
- Cognitive load: Is the interface understandable and predictable?
- Keyboard-only navigation quality: Can users accomplish tasks efficiently?
Combine automated tools with:
- Manual audits using tools like Lighthouse, WAVE, or browser extensions
- Real user feedback from people with disabilities
- Inclusive design reviews during the design phase
The Business Case for Accessibility
Beyond the moral imperative, accessibility is good for business:
- Legal Compliance: In the US, the ADA applies to websites. The EU has the EAA (European Accessibility Act). Many countries have similar regulations.
- Market Reach: Over 1 billion people globally live with disabilities. Accessible sites can serve a larger audience.
- SEO Benefits: Many accessibility best practices (semantic HTML, alt text, clear headings) also improve search rankings.
- Better UX for All: Accessible design often leads to cleaner, more usable interfaces for everyone.
Conclusion
Accessibility testing is no longer optional�it's a professional and ethical responsibility. By integrating tools like Playwright and axe-core into your development workflow, you can catch and fix accessibility issues early, at scale, and continuously.
Start small: add one accessibility test to your suite. Scan your homepage. Fix the violations. Expand to more pages and user flows. Over time, you'll build a culture of inclusivity and a product that works for everyone.
Ready to build a more accessible web? Sign up for ScanlyApp and integrate automated accessibility testing into your QA workflow today.
