All posts
19 Sep 2026

Automating Accessibility: Catching WCAG Violations in CI with Playwright and axe-core

Learn how to integrate axe-core into your Playwright end-to-end testing suite to automatically catch WCAG accessibility violations before they ever reach production.

Automating Accessibility: Catching WCAG Violations in CI with Playwright and axe-core

Web accessibility is no longer an afterthought; it is a fundamental requirement for modern web applications. Ensuring that users with disabilities can navigate, interact with, and understand your software is both a legal imperative and a moral obligation. Yet, accessibility (a11y) regressions frequently slip into production because manual screen-reader testing or periodic audits happen too late in the development lifecycle.

What if you could catch missing alt attributes, insufficient color contrast ratios, and broken ARIA roles before a single line of code is merged to your main branch?

In this tutorial, we will explore how to combine Playwright—a modern end-to-end testing framework—with axe-core, the industry-standard accessibility testing engine. By the end of this guide, you will have a fully automated accessibility testing pipeline running on every pull request.


Why Automated Accessibility Testing Matters

Manual accessibility audits are essential for evaluating complex keyboard navigation and screen reader nuance, but they do not scale well. Developers often introduce regressions daily: a refactored button loses its accessible label, or a new design system token fails color contrast guidelines.

Automating accessibility checks within your end-to-end (E2E) tests bridges this gap. Because E2E tests already drive real browsers through critical user flows (like signing up, checking out, or updating user settings), injecting an accessibility scanner at each step allows you to audit the application in its actual rendered state.

The Tooling Stack

  • Playwright: Fast, reliable, and capable cross-browser automation.
  • axe-core: A lightweight, highly accurate JavaScript accessibility testing library maintained by Deque Systems.
  • @axe-core/playwright: A specialized wrapper that injects axe-core into Playwright browser contexts, allowing us to scan pages with a single method call.

Setting Up the Project

Let’s start by installing the necessary dependencies in an existing Playwright project. If you are starting from scratch, you can initialize Playwright via npm init playwright@latest.

Run the following command to install the axe-core Playwright integration:

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

Ensure your playwright.config.ts is configured to target your local development server or staging environment:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Writing Your First Accessibility Test

Let’s write a test that navigates to our application’s dashboard and runs an accessibility scan using axe-core.

Create a new file named e2e/accessibility.spec.ts:

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility Audits', () => {
  test('dashboard should not have any automatically detectable WCAG violations', async ({ page }) => {
    // 1. Navigate to the target page
    await page.goto('/dashboard');

    // 2. Run the axe accessibility analysis
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
      .analyze();

    // 3. Assert that no violations were found
    expect(accessibilityScanResults.violations).toEqual([]);
  });
});

Breaking Down the Code

  1. new AxeBuilder({ page }): Initializes the axe wrapper, binding it to the current Playwright page instance.
  2. .withTags(...): Filters the rules to check against specific standards. Here, we target WCAG 2.0 and 2.1 Level A and AA criteria, which cover the legal compliance standard for most organizations.
  3. .analyze(): Injects the axe-core script into the browser, runs the rules against the current DOM tree, and returns a comprehensive report object.
  4. expect(...).toEqual([]): Fails the test if any violations are detected, printing a detailed breakdown in the test output.

Scanning User Flows and Dynamic States

Accessibility bugs often hide behind user interactions—modals, dropdowns, multi-step forms, and loading states. Because we are using Playwright, we can manipulate the DOM state before running our accessibility scan.

Here is an example of testing a modal dialog when it is open:

test('settings modal should be accessible when open', async ({ page }) => {
  await page.goto('/settings');

  // Trigger a modal to open
  await page.click('button#open-settings-modal');
  
  // Wait for the modal to be visible
  await page.locator('[role="dialog"]').waitFor();

  // Scan only the modal, or scan the whole page with specific focus
  const accessibilityScanResults = await new AxeBuilder({ page })
    .include('[role="dialog"]') // Scope the scan to just the modal
    .analyze();

  expect(accessibilityScanResults.violations).toEqual([]);
});

Tip: Use .include() to scope scans to newly rendered components or complex widgets, saving execution time and keeping failure logs concise.


Handling Known Issues and Gradual Adoption

When introducing accessibility testing to an existing codebase, you will likely encounter dozens of pre-existing violations. Fixing them all at once can halt feature development.

Fortunately, axe-core allows you to temporarily exclude specific rules or known element violations using .disableRules() or by ignoring specific IDs while you work through a remediation backlog.

test('legacy checkout page with acknowledged warnings', async ({ page }) => {
  await page.goto('/checkout');

  const results = await new AxeBuilder({ page })
    // Temporarily disable contrast checks while design system is updated
    .disableRules(['color-contrast'])
    .analyze();

  expect(results.violations).toEqual();
});

However, use this sparingly. The ultimate goal is to remove exclusions as technical debt is paid down.


Integrating with CI/CD (GitHub Actions)

Now that we have our automated accessibility tests written, let’s run them automatically inside a GitHub Actions workflow whenever a pull request is opened or updated.

Create a file at .github/workflows/accessibility.yml:

name: Accessibility Tests

on:
  pull_request:
    branches: [main, master]
  push:
    branches: [main, master]

jobs:
  test-a11y:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run Playwright accessibility tests
        run: npx playwright test e2e/accessibility.spec.ts

      - name: Upload Playwright Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

What happens when a test fails in CI?

If a developer merges a PR that introduces an accessibility violation (e.g., an icon button without an aria-label), the GitHub Action will fail, blocking the merge.

The developer can download the uploaded Playwright HTML report artifact to inspect the exact DOM element that failed, along with links to the Deque University guide on how to fix that specific WCAG violation.


Conclusion

Integrating axe-core into your Playwright testing suite transforms accessibility from a manual, stressful guessing game into an automated safety net. By shifting accessibility checks left into your CI pipeline, you ensure that every feature shipped is inclusive by default.

Remember: automated testing tools catch roughly 30% to 50% of all accessibility issues (primarily programmatic errors like missing labels, duplicate IDs, and contrast failures). Combine automated CI checks with keyboard-only testing and periodic screen reader evaluations for a truly robust accessibility strategy.

More posts