Accessibility (a11y) Testing — Building for Everyone
1 in 4 adults lives with a disability. If your app isn't accessible, you're locking them out. Learn how to integrate automated WCAG audits into your Playwright tests to ensure your UI is usable by everyone.
📖 The Story: The Invisible Wall
A major university launched its new online registration portal. The functional tests passed. The visual tests passed. But when classes opened, the disability services office was flooded with calls. Blind students couldn't register.
The QA team investigated. A developer had used a `div` with an `onclick` handler instead of a `button` for the "Submit Registration" action. The visual UI looked perfect, and Playwright's `click()` could interact with it. But screen readers couldn't see it, and keyboard users couldn't tab to it. It was an invisible wall.
The team integrated `@axe-core/playwright` into their suite. The very next run, Axe flagged the `div` as a critical ARIA violation. They changed it to a `button`. The wall came down.
Accessibility isn't a feature; it's a baseline. If it's not accessible, it doesn't work.
🎯 Why a11y Testing?
Inclusivity
Ensure users with visual, motor, or cognitive disabilities can use your application.
Legal Compliance
Avoid ADA and WCAG lawsuits, which cost companies millions annually.
SEO Benefits
Search engine crawlers rely on the same semantic HTML that screen readers do.
Code Quality
Forcing semantic HTML improves overall code health and maintainability.
🌍 Real World Example
A developer adds a custom dropdown component. It looks great. However, they forgot to add `aria-expanded` and `aria-controls` attributes. A sighted user can click it and see the options. A screen reader user just hears "Button," with no indication that it opens a menu. Axe catches this missing ARIA binding instantly.
🧒 Explain Like I'm 10
Imagine building an awesome treehouse. You put in a rope ladder. But one of your friends uses a wheelchair. They can't climb the rope ladder. They are locked out.
Accessibility testing is like checking your treehouse to make sure it has a ramp or an elevator, so everyone can get in and play. We use a tool (Axe) to automatically check if our website has any "rope ladders" that lock people out.
🎓 Professional Explanation
"a11y" stands for accessibility (11 letters between 'a' and 'y'). While Playwright has a built-in `page.accessibility.snapshot()` to read the accessibility tree, it does not perform WCAG compliance auditing.
To audit compliance, we use `@axe-core/playwright`. Axe is a rule-based engine that injects a JavaScript script into the browser. This script analyzes the DOM, computes styles, and checks against WCAG (Web Content Accessibility Guidelines) rulesets. It then returns a JSON report of any violations (e.g., color contrast issues, missing alt text, invalid ARIA).
📊 Automated vs Manual Testing
What Axe Can and Cannot Do
✅ Automated (Axe)
❌ Manual (Human)
Axe catches ~40% of issues. The rest requires manual keyboard/screen reader testing.
🪓 Installing @axe-core/playwright
First, install the Axe Playwright package.
npm install --save-dev @axe-core/playwright
📸 The Basic Audit Test
Here is the standard pattern for running an accessibility audit on a page.
import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('homepage should pass WCAG accessibility audits', async ({ page }) => { await page.goto('https://your-app.com/'); const accessibilityScanResults = await new AxeBuilder({ page }) // .withTags(['wcag2a', 'wcag2aa']) // Optional: specify WCAG level // .exclude('.third-party-widget') // Optional: exclude problematic 3rd party UI .analyze(); // Assert that there are no violations expect(accessibilityScanResults.violations).toEqual([]); });
How it works: `AxeBuilder` injects the axe-core script into the current page. `.analyze()` runs the rules asynchronously and returns an object. The `violations` array contains detailed objects for every failure. Asserting it is empty means the page passed.
🏷️ Auditing Specific WCAG Levels
By default, Axe runs all its rules. If you only want to check for specific legal compliance (like WCAG 2.1 Level AA), use `withTags`.
const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) // Level A and Level AA .analyze();
⚠️ Common Mistakes
Mistake 1: Running Axe on the Wrong State
// ❌ BAD: Running Axe before the modal is open await page.goto('/dashboard'); await new AxeBuilder({ page }).analyze(); // Only checks dashboard await page.click('Open Modal'); // ✅ GOOD: Run Axe AFTER the modal is open to check the modal's a11y await page.goto('/dashboard'); await page.click('Open Modal'); await new AxeBuilder({ page }).analyze(); // Checks modal overlay
Mistake 2: Excluding Third-Party Widgets Silently
If you use `.exclude()` to ignore an iframe or ad widget, document it. Silent exclusions hide real accessibility debt from product managers.
✅ Best Practices
- Run a11y tests per component, not just the whole page. It's easier to pinpoint the broken component.
- Test different states. Run Axe when modals are open, accordions are expanded, and form errors are displayed.
- Don't ignore violations. If Axe reports a violation, fix it. If you can't, file a ticket.
- Combine with Playwright keyboard tests. Use `
page.keyboard.press('Tab')` to ensure focus moves logically.
🏗️ Senior Engineer Deep Dive
The Axe Core Engine
Axe doesn't just look at HTML attributes; it computes the accessibility tree. For example, to check color contrast, it doesn't just read `color: gray`. It uses `window.getComputedStyle()` to get the final rendered RGB values, then checks the background color behind it, calculating the luminance ratio to ensure it meets WCAG 4.5:1 standards. This makes Axe incredibly accurate but slightly CPU intensive.
Performance in CI
Running an Axe audit on a complex page can take 500ms to 2 seconds. If you have 100 a11y tests, this adds up. The enterprise pattern is to combine a11y tests into a single "Accessibility Spec" that runs once per deploy, rather than attaching `AxeBuilder` to every single functional test.
💼 Interview Questions
Show Answer
Show Answer
Show Answer
Show Answer
🏋️ Practical Exercise
Run Your First Audit
- Install `@axe-core/playwright` in your project.
- Write a test that navigates to a public site (e.g., `https://example.com`).
- Add the `AxeBuilder` code from the "Basic Audit Test" section above.
- Run the test. If the site passes, it will be green.
- Change the URL to a site known for bad a11y, or remove the `
alt` attribute from an image on your local app. Run it again and see the failure report in the terminal.
🚀 Mini Project
🏗️ The Modal Accessibility Check
- Create a test that opens a modal dialog on your app.
- Before running Axe, assert that the modal is visible.
- Run `AxeBuilder` against the page while the modal is open.
- Ensure the test fails if the modal lacks `role="dialog"` or `aria-modal="true"`.
- Fix the modal code to pass the test.
📝 Quiz
Test your understanding. Click an option to check your answer.
❓ FAQ
Q: Can Axe test inside iframes?
A: Yes, `@axe-core/playwright` automatically traverses same-origin iframes. If the iframe is cross-origin, you may need to handle frame permissions.
Q: Should I run a11y tests on every commit?
A: It depends on suite speed. If Axe slows down your PR builds too much, move a11y tests to a nightly CI job or a separate project that runs on merges to main.
Q: Does Axe work with Shadow DOM?
A: Yes, Axe-core has excellent support for traversing open Shadow DOM. Closed Shadow DOM cannot be inspected by any automated tool.
📦 Summary
🎯 Key Takeaways
- Accessibility ensures everyone can use your app.
- Use `@axe-core/playwright` to run automated WCAG audits inside E2E tests.
- Run audits on different UI states (e.g., modals open, errors showing).
- Automated tools catch ~40% of issues. Manual keyboard/screen reader testing is still required.
- Use `.withTags(['wcag2a', 'wcag2aa'])` to audit against specific legal standards.
- Use `.exclude()` to skip third-party widgets you cannot fix.
