📖 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)
Missing alt text
Color contrast ratios
Invalid ARIA roles
Missing form labels
Duplicate IDs
❌ Manual (Human)
Keyboard navigation flow
Screen reader accuracy
Focus visibility on complex widgets
Content meaning clarity

Axe catches ~40% of issues. The rest requires manual keyboard/screen reader testing.

🪓 Installing @axe-core/playwright

First, install the Axe Playwright package.

bash
npm install --save-dev @axe-core/playwright

📸 The Basic Audit Test

Here is the standard pattern for running an accessibility audit on a page.

typescript
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`.

typescript
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

typescript · ❌ 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

  1. Run a11y tests per component, not just the whole page. It's easier to pinpoint the broken component.
  2. Test different states. Run Axe when modals are open, accordions are expanded, and form errors are displayed.
  3. Don't ignore violations. If Axe reports a violation, fix it. If you can't, file a ticket.
  4. 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

Beginner
What does "a11y" stand for?
Show Answer
"a11y" is a numeronym for "accessibility". The "11" represents the 11 letters between the 'a' and the 'y'. It is commonly used in tech to save space when typing.
Intermediate
How do you perform accessibility testing in Playwright?
Show Answer
While Playwright can read the accessibility tree, I use the `@axe-core/playwright` library to perform WCAG audits. I instantiate an `AxeBuilder` with the page object, call `.analyze()`, and assert that the `violations` array in the result is empty.
Advanced
What percentage of accessibility issues can automated tools like Axe catch?
Show Answer
Automated tools catch roughly 30-40% of accessibility issues. They are great at finding missing ARIA, bad contrast, and missing labels, but they cannot test actual screen reader behavior or complex keyboard navigation flows. Manual testing is still required.
Scenario
Your Axe test fails on a third-party analytics widget that you cannot edit. How do you handle this in your test?
Show Answer
I use the `.exclude()` method on the `AxeBuilder`, passing a locator that targets the third-party widget. This tells Axe to ignore that specific DOM node. I would also document this exclusion and file a ticket with the vendor to fix their a11y.

🏋️ Practical Exercise

🎯 Hands-On Practice Easy

Run Your First Audit

  1. Install `@axe-core/playwright` in your project.
  2. Write a test that navigates to a public site (e.g., `https://example.com`).
  3. Add the `AxeBuilder` code from the "Basic Audit Test" section above.
  4. Run the test. If the site passes, it will be green.
  5. 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

  1. Create a test that opens a modal dialog on your app.
  2. Before running Axe, assert that the modal is visible.
  3. Run `AxeBuilder` against the page while the modal is open.
  4. Ensure the test fails if the modal lacks `role="dialog"` or `aria-modal="true"`.
  5. Fix the modal code to pass the test.

📝 Quiz

Test your understanding. Click an option to check your answer.

1. What library is commonly used with Playwright for WCAG auditing?
A) `@playwright/a11y`
B) `jest-axe`
C) `@axe-core/playwright`
D) `lighthouse`
Answer: C — `@axe-core/playwright` is the official axe-core package built specifically for the Playwright test runner.
2. Approximately what percentage of accessibility issues can automated tools catch?
A) 100%
B) 80%
C) 30-40%
D) 5%
Answer: C — Automated tools catch about a third of issues. Manual testing is required for the rest.
3. How do you tell Axe to only check for WCAG 2.1 Level AA rules?
A) `.withRules(['wcag2aa'])`
B) `.withTags(['wcag2aa'])`
C) `.setLevel('AA')`
D) `.configure({ level: 'AA' })`
Answer: B — The `.withTags()` method accepts an array of rule tags, such as `'wcag2a'` and `'wcag2aa'`.
4. What does `expect(accessibilityScanResults.violations).toEqual([])` do?
A) Fails the test if there are any accessibility violations
B) Logs the violations to the console
C) Automatically fixes the violations
D) Ignores minor violations
Answer: A — By asserting the violations array is empty, the test will fail if Axe finds any a11y issues.
5. When should you run an Axe audit on a modal?
A) Before clicking the button to open it
B) After the modal is fully open and visible
C) After closing the modal
D) It doesn't matter
Answer: B — Axe analyzes the current DOM. The modal's DOM must be present and visible to be audited correctly.
6. Which of the following can Axe automatically detect?
A) A button that doesn't do anything when clicked
B) Missing alt text on an image
C) Confusing content wording
D) A video lacking audio descriptions
Answer: B — Axe can easily detect missing semantic attributes like `alt` text. It cannot judge content meaning.
7. How can you exclude a problematic third-party widget from an Axe scan?
A) `.ignore('.widget')`
B) `.exclude('.widget')`
C) `.skip('.widget')`
D) `.disableRules('widget')`
Answer: B — The `.exclude()` method accepts a CSS selector or locator and skips analyzing that subtree.
8. Does Playwright have built-in WCAG auditing tools?
A) Yes, `page.audit()`
B) Yes, but only for ARIA
C) No, it only has `page.accessibility.snapshot()` to read the a11y tree
D) No, Playwright does not support accessibility at all
Answer: C — Playwright can read the accessibility tree, but it relies on third-party libraries like Axe for compliance auditing.
9. Why is color contrast testing important?
A) It makes the site look prettier
B) It ensures users with visual impairments (like color blindness) can read the text
C) It improves page load speed
D) It helps with SEO
Answer: B — Low contrast makes text unreadable for users with low vision or color blindness. Axe calculates the luminance ratio automatically.
10. What is a limitation of automated a11y testing?
A) It is too slow for CI
B) It requires a paid license
C) It cannot verify logical keyboard tab order or screen reader announcements
D) It only works on Windows
Answer: C — Tools can verify a node is focusable, but a human must test if the tab order makes logical sense.

❓ 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.