Overview

pa11y is a Node-based accessibility scanner that runs WCAG 2.1 AA checks against any URL. This guide installs it, writes a project config, reads the output, fixes the five most common violations, and adds pa11y-ci to a GitHub Actions job. For semantic HTML patterns that prevent violations before they happen, see html-aria-patterns.

Prerequisites

  • Node 22 installed. Check with node -v.
  • A site running locally or accessible at a public URL. pa11y fetches the page through a headless Chromium browser.
  • npm in the project package.json devDependencies, or installed globally.

Steps

1. Install pa11y and pa11y-ci

npm install --save-dev pa11y pa11y-ci

For a quick one-off scan without a project install:

npx pa11y https://example.com

2. Write .pa11yci.json

Place .pa11yci.json at the project root:

{
  "defaults": {
    "standard": "WCAG2AA",
    "timeout": 10000,
    "wait": 500,
    "reporters": ["cli", "json"],
    "chromeLaunchConfig": {
      "args": ["--no-sandbox", "--disable-setuid-sandbox"]
    }
  },
  "urls": [
    "http://localhost:8080",
    "http://localhost:8080/about",
    "http://localhost:8080/blog"
  ]
}

wait: 500 gives React and other client-rendered frameworks 500 ms to hydrate before the scan. Increase to 1000 for heavy SPAs. --no-sandbox is required in Docker and most CI environments.

3. Run a scan

Start your dev server, then run:

npx pa11y-ci --config .pa11yci.json

Example output:

Running Pa11y on 3 URLs:
 > http://localhost:8080 - 0 errors
 > http://localhost:8080/about - 3 errors
 > http://localhost:8080/blog - 1 error

4 errors found across 2 URLs

Exit code 1 means violations were found. Exit code 0 means clean. CI jobs gate on the exit code.

To see the full violation details:

npx pa11y http://localhost:8080/about --standard WCAG2AA

4. Read the output

Each violation reports:

Error: Image element does not have an alt attribute (WCAG 1.1.1 A)
  Element: <img src="hero.jpg">
  Context: <img src="hero.jpg">
  Rule: image-alt
  Selector: img[src="hero.jpg"]

The Selector field lets you jump directly to the element in DevTools.

5. Fix common issues

Missing alt text (WCAG 1.1.1)

<!-- Before -->
<img src="hero.jpg">
 
<!-- After: descriptive alt for informative images -->
<img src="hero.jpg" alt="Developer typing on a laptop in a coffee shop">
 
<!-- After: empty alt for decorative images -->
<img src="divider.png" alt="">

Missing form labels (WCAG 1.3.1)

<label for="email">Email</label>
<input id="email" type="email">

Low color contrast (WCAG 1.4.3)

Use a contrast checker (browser DevTools or axe DevTools extension) to find passing foreground/background pairs. WCAG AA requires 4.5:1 for normal text, 3:1 for large text.

Missing landmark regions (WCAG 1.3.6)

Wrap page sections in <main>, <nav>, <header>, <footer>. See html-semantic-elements.

Missing <html lang> (WCAG 3.1.1)

<html lang="en">

See html-aria-patterns for the full ARIA pattern reference.

6. Integrate with GitHub Actions CI

# .github/workflows/a11y.yml
name: Accessibility
on: [push, pull_request]
jobs:
  pa11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm run build
      - name: Serve and scan
        run: |
          npx serve public -l 8080 &
          sleep 3
          npx pa11y-ci --config .pa11yci.json

The sleep 3 gives the static server time to bind. For a dev server that takes longer, use wait-on:

npx wait-on http://localhost:8080 && npx pa11y-ci --config .pa11yci.json

Verify it worked

# 1. pa11y is installed.
npx pa11y --version
 
# 2. A known-good page returns exit 0.
npx pa11y https://example.com --standard WCAG2AA; echo "Exit: $?"
 
# 3. pa11y-ci config is valid JSON.
node -e "require('./.pa11yci.json'); console.log('Valid JSON')"
 
# 4. CI job passes on the current branch.
# Push to GitHub and check the Actions tab.

Common errors

  • Error: Failed to launch the browser process. Missing --no-sandbox flag in chromeLaunchConfig. Add the two args to the defaults block.
  • Timeout exceeded. The page takes too long to load. Increase timeout to 30000 ms or check that the dev server is actually running.
  • 0 URLs specified. The urls array in .pa11yci.json is empty or the config path is wrong. Pass --config explicitly.
  • False positives on dynamically injected content. pa11y scans the DOM at page-load time. Increase wait to give the framework time to render.
  • pa11y-ci not found. Run npm ci first, or prefix with npx.