---
title: "How to run pa11y locally"
slug: "run-pa11y-locally"
category: "howto"
tags: ["howto", "accessibility", "pa11y", "a11y", "tutorial", "testing", "ci", "frontend"]
status: "stable"
last_updated: 2026-05-14
summary: "Install pa11y, write a .pa11yci.json config, run a scan, fix the common issues, and plug pa11y-ci into a GitHub Actions workflow."
related: ["[[frontend/accessibility]]", "[[frontend/html-aria-patterns]]", "[[frontend/html-semantic-elements]]", "[[coding/testing]]", "[[howto/audit-site-for-core-web-vitals]]", "[[cheatsheets/http-status-codes]]"]
---

## 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 [[frontend/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

```bash
npm install --save-dev pa11y pa11y-ci
```

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

```bash
npx pa11y https://example.com
```

### 2. Write `.pa11yci.json`

Place `.pa11yci.json` at the project root:

```json
{
  "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:

```bash
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:

```bash
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)**

```html
<!-- 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)**

```html
<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 [[frontend/html-semantic-elements]].

**Missing `<html lang>` (WCAG 3.1.1)**

```html
<html lang="en">
```

See [[frontend/html-aria-patterns]] for the full ARIA pattern reference.

### 6. Integrate with GitHub Actions CI

```yaml
# .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`:

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

## Verify it worked

```bash
# 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`.

## Related

- [[frontend/accessibility]]
- [[frontend/html-aria-patterns]]
- [[frontend/html-semantic-elements]]
- [[coding/testing]]
- [[howto/audit-site-for-core-web-vitals]]
