DEV Community

AgentKit
AgentKit

Posted on • Originally published at blog.a11yfix.dev

How to Set Up Automated Accessibility Testing in GitHub Actions (Copy-Paste Config)

Originally published at A11yFix.


Last month I inherited a project where accessibility was "on the roadmap." Translation: nobody had touched it. Rather than rely on manual audits that happen once a quarter (if you're lucky), I set up automated accessibility testing that runs on every pull request. It took about 20 minutes, and now no PR merges if it introduces accessibility violations.

Here's exactly how to do it.

Why axe-core + Playwright?

There are plenty of accessibility testing tools out there, but this combination hits a sweet spot:

  • axe-core is the industry standard engine behind most accessibility tools. It catches real WCAG violations, not theoretical ones.
  • Playwright gives you a real browser environment, so you're testing what users actually see -- not just static HTML.
  • Both are free and open source.

Together they catch around 30-40% of WCAG 2.1 issues automatically. That won't replace manual testing, but it prevents regressions and catches the low-hanging fruit before a human ever looks at the page.

Step 1: Install the dependencies

npm install -D @axe-core/playwright @playwright/test
npx playwright install chromium
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the accessibility test file

Create tests/accessibility.spec.ts:

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

const pagesToTest = [
  { name: "Home", path: "/" },
  { name: "About", path: "/about" },
  // Add your routes here
];

for (const { name, path } of pagesToTest) {
  test(`${name} page should have no accessibility violations`, async ({
    page,
  }) => {
    await page.goto(path);

    const results = await new AxeBuilder({ page })
      .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
      .analyze();

    const violations = results.violations.map((v) => ({
      id: v.id,
      impact: v.impact,
      description: "v.description,"
      nodes: v.nodes.length,
    }));

    if (violations.length > 0) {
      console.log("Accessibility violations found:");
      console.log(JSON.stringify(violations, null, 2));
    }

    expect(results.violations).toEqual([]);
  });
}
Enter fullscreen mode Exit fullscreen mode

A few things to note:

  • withTags filters to WCAG 2.1 Level AA rules. That's the standard most regulations (including the European Accessibility Act) point to.
  • The test logs structured violation data before failing, so you can see exactly what's wrong in the CI output.
  • Adding new pages is just adding an entry to the array.

Step 3: Configure Playwright

If you don't already have a playwright.config.ts, create one:

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

export default defineConfig({
  testDir: "./tests",
  use: {
    baseURL: "http://localhost:3000",
  },
  webServer: {
    command: "npm run start",
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});
Enter fullscreen mode Exit fullscreen mode

Adjust the command and port to match your project. If you're using Next.js, npm run start works after a build. For Vite, use npm run preview. The key is that Playwright spins up your server automatically during CI.

Step 4: The GitHub Actions workflow

Create .github/workflows/accessibility.yml:

name: Accessibility Tests

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

jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - 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 chromium

      - name: Build
        run: npm run build

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

      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: a11y-report
          path: test-results/
          retention-days: 7
Enter fullscreen mode Exit fullscreen mode

That's it. Copy that file into your repo and accessibility testing runs on every PR.

The if: failure() on the upload step means you only get artifacts when tests fail -- so you can dig into the details without cluttering up passing runs.

Step 5: Handle violations without blocking everything

Sometimes you need to ship a feature while you fix an existing accessibility issue. You can exclude specific rules temporarily:

const results = await new AxeBuilder({ page })
  .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
  .disableRules(["color-contrast"]) // TODO: Fix by sprint 23
  .analyze();
Enter fullscreen mode Exit fullscreen mode

I recommend adding a comment with a deadline and tracking these exclusions. They have a tendency to become permanent if you don't.

What this catches (and what it doesn't)

Automated testing reliably catches:

  • Missing alt text on images
  • Missing form labels
  • Broken ARIA attributes
  • Insufficient color contrast
  • Missing document language
  • Incorrect heading hierarchy

It won't catch:

  • Whether alt text is actually meaningful
  • Keyboard navigation flow issues
  • Screen reader announcement quality
  • Complex interactive widget behavior

That's why automated testing is a safety net, not a replacement for real accessibility work. But a safety net that runs on every PR is a lot better than a manual audit that happens twice a year.

Making it work with your stack

If you're using a static site or SSR framework, the config above works as-is. For SPAs, make sure Playwright waits for your app to hydrate. You might need to add a waitUntil: 'networkidle' option to page.goto(), or wait for a specific element to appear before running the analysis.

For monorepos, point the testDir and webServer config at the right package, and you're set.


Automated accessibility testing won't make your site fully accessible overnight. But it draws a line: from this point forward, we don't ship new violations. That's a meaningful starting point.

If you're working on accessibility compliance, I put together a free 10-point EAA quick-check: Get the free checklist

Top comments (0)