Accessibility auditing is a structured process for validating that a digital product meets the needs of users with disabilities—visual, auditory, motor, and cognitive impairments—across its interfaces. It matters most during development sprints, before major releases, and when onboarding new teams to a shared design system. Audits are not one-off checks; they are repeatable, deterministic workflows that uncover inconsistencies, omissions, and performance bottlenecks in how content and interactions are perceived and navigated.

Preparing for an Audit

Before running any tool or manual review, establish a consistent audit baseline. Begin by defining the audit scope: which pages, components, or user flows are in scope. For a web application, this might be all routes under /dashboard, /settings, and /reports. Use a standard list of test devices: desktop (Chrome, Firefox, Safari), tablet (iOS Safari, Android Chrome), and mobile (Android Chrome, iOS Safari), plus screen reader and keyboard-only sessions.

The core artifact is a accessibility-audit.yml configuration file in the project root. It defines the expected outcomes and thresholds for automated checks.


audit:
  scope:
    - "/dashboard"
    - "/settings/*"
    - "/reports/:id"
  devices:
    - desktop
    - tablet
    - mobile
  tools:
    - axe-core
    - lighthouse
    - pa11y
  thresholds:
    critical:
      wcag-2.1: "AA"
      total-issues: 10
      time-to-first-contentful-paint: 1500
    warning:
      total-issues: 25
      time-to-first-contentful-paint: 2500
  targets:
    - name: "Main dashboard"
      url: "https://app.example.com/dashboard"
      roles: [ "admin", "editor", "viewer" ]
      expectations:
        - label: "Screen reader navigation"
          checks:
            - "All interactive elements have accessible names"
            - "Landmarks are properly nested"
            - "Focus is visible on all interactive elements"
        - label: "Keyboard-only workflow"
          checks:
            - "Tab order matches visual flow"
            - "All modals are keyboard accessible"
            - "Skip-to-content link is present"

This file drives the entire audit process. It becomes the source of truth for both automated pipelines and manual reviews. When a team member runs npm run audit, the script reads this file and orchestrates the full audit.

Running Automated Audits

Use a consistent set of open-source tools that produce machine-readable output. The primary stack is axe-core, Lighthouse, and pa11y. Each runs independently but outputs structured data that can be aggregated and validated.

Run axe-core via axe CLI:

npx axe --config ./accessibility-audit.yml --reporter json --output ./reports/axe-results.json

The --config flag loads the accessibility-audit.yml file, which includes axe-specific options:

axe:
  options:
    runOnly:
      type: "tag"
      values:
        - "wcag2a"
        - "wcag2aa"
        - "best-practice"
    timeout: 10000
    include: [ "main", "header", "nav" ]
    exclude: [ ".skip-axe" ]

The runOnly configuration ensures only relevant rules are executed. The timeout setting prevents slow audits from failing due to long-running asynchronous content. The include and exclude selectors target specific DOM regions, avoiding redundant checks.

Lighthouse is invoked with lighthouse CLI, with a custom config:

npx lighthouse https://app.example.com/dashboard \
  --output=json \
  --output-path=./reports/lighthouse-results.json \
  --config-path=./lighthouse-config.json \
  --chrome-flags="--headless --no-sandbox --disable-gpu"

The lighthouse-config.json file defines audit categories and settings:

{
  "extends": "lighthouse:default",
  "settings": {
    "emulatedFormFactor": "desktop",
    "screenEmulation": {
      "width": 1280,
      "height": 800,
      "deviceScaleFactor": 1,
      "mobile": false
    },
    "onlyCategories": [
      "performance",
      "accessibility",
      "best-practices",
      "seo"
    ],
    "a11y": {
      "include": [
        "aria-allowed-attr",
        "aria-allowed-role",
        "aria-roles",
        "aria-valid-attr-value",
        "color-contrast",
        "heading-levels",
        "landmark-roles",
        "link-name"
      ],
      "exclude": [
        "aria-hidden-body",
        "aria-hidden-ancestor"
      ],
      "config": {
        "colorContrast": {
          "threshold": 4.5
        },
        "headingLevel": {
          "minLevel": 1,
          "maxLevel": 6
        }
      }
    }
  }
}

The a11y section in lighthouse-config.json enables fine-grained control over which rules are run and their parameters. The colorContrast threshold of 4.5 enforces WCAG AA standards. The headingLevel rule validates that headings are properly nested, and the config object overrides defaults.

pa11y runs as a command-line service with pa11y-ci:

pa11y-ci --config ./pa11y-config.json --output ./reports/pa11y-results.json

The pa11y-config.json file defines the audit pipeline:

{
  "url": "https://app.example.com",
  "standard": "WCAG2AA",
  "ignore": [
    "aria-allowed-attr",
    "aria-allowed-role"
  ],
  "include": [
    "aria-roles",
    "color-contrast",
    "focus-traps"
  ],
  "checks": [
    {
      "name": "Main navigation",
      "url": "/dashboard",
      "expected": {
        "total": 30,
        "critical": 5,
        "warning": 10
      }
    },
    {
      "name": "User profile form",
      "url": "/settings/profile",
      "expected": {
        "total": 20,
        "critical": 3,
        "warning": 8
      }
    }
  ],
  "screenshot": true,
  "screenshotPath": "./screenshots/"
}

pa11y-ci runs each check against its defined URL, collects results, and compares actual counts to expected ones. If a form has more than five critical issues, the audit fails. The screenshot option captures visual evidence for every test, useful for reviewing failures.

Aggregating and Validating Results

After running all tools, combine their outputs into a single summary using a script or CI job. Use jq to extract key metrics from JSON reports and produce a unified audit result:

jq -s '
  [
    .[0].results | map(select(.category == "Accessibility")),
    .[1].audits | to_entries | map(select(.key | test("aria|color|heading|landmark"))),
    .[2].results | map(.issues)
  ] | add |
  {
    totalIssues: length,
    criticalIssues: [map(select(.severity == "critical"))] | length,
    issuesByType: reduce .[] as $i ({}; .[$i.type] += 1),
    avgTimeToFirstContentfulPaint: [map(.timing) | add / length] | .[0]
  }
' \
  ./reports/axe-results.json \
  ./reports/lighthouse-results.json \
  ./reports/pa11y-results.json \
  > ./reports/audit-summary.json

This jq pipeline combines results from all three tools into a single, actionable summary. It computes the total number of issues, counts critical issues, aggregates issues by type (e.g., color-contrast, aria-roles), and computes average time to first contentful paint.

Validate the aggregated results against the accessibility-audit.yml thresholds:

npx @acme/audit-validator ./reports/audit-summary.json

The audit-validator tool reads the accessibility-audit.yml thresholds and outputs a validation report:

{
  "status": "failed",
  "message": "Critical issues exceed threshold of 10",
  "details": [
    {
      "check": "total-issues",
      "expected": 10,
      "actual": 18,
      "severity": "critical"
    },
    {
      "check": "time-to-first-contentful-paint",
      "expected": 1500,
      "actual": 2100,
      "severity": "warning"
    }
  ],
  "summary": "18 total issues, 8 critical, 10 warning"
}

This report becomes part of the audit artifact and can be used to gate CI/CD pipelines. A failed audit stops a deployment unless approved manually.

Manual Review and Pairing

Automated tools catch surface-level issues but miss nuanced experiences. Conduct manual audits using a checklist and screen reader.

Use NVDA on Windows or VoiceOver on macOS. Start with a screen reader-only session on the main dashboard. Navigate using only keyboard and screen reader, without mouse.

Create a checklist in manual-audit.md:

# Manual Accessibility Audit: Dashboard

## Screen Reader Navigation
- [ ] All buttons have visible focus
- [ ] All form fields have associated labels
- [ ] Landmarks are announced (e.g., "Main content", "Navigation")
- [ ] Page title and heading hierarchy are correctly announced
- [ ] Dynamic content updates are announced (e.g., "New message received")

## Keyboard-Only Workflow
- [ ] Tab order matches visual flow
- [ ] Focus is visible (outline or background)
- [ ] Keyboard shortcuts are documented and discoverable
- [ ] Modal dialogs are accessible (ESC to close, focus trapped)
- [ ] Skip-to-content link is present and functional

## Cognitive and Motor Considerations
- [ ] All interactive elements have sufficient tap target (at least 44px)
- [ ] Hover and focus states are distinct
- [ ] Text is readable at 120% zoom
- [ ] Color contrast meets 4.5:1 for text
- [ ] All icons have alternative text or are accessible via aria-label

Pair two team members: one as the “user” navigating with screen reader and keyboard, the other as the “auditor” logging observations and issues in real time. Use screen or tmux to record the session.

When a failure occurs, capture the exact state:

  • Current screen, focus, and screen reader output
  • Full DOM tree (via browser devtools)
  • Log of keyboard events (e.g., Tab sequences, Enter presses)
  • Audio recording of the screen reader narration

Handling Common Failure Modes

Silent failures are common. One critical issue is misconfigured axe-core include and exclude selectors. A component with aria-hidden="true" is excluded, but the exclude selector is ".skip-axe" instead of "[aria-hidden='true']". The audit passes, but the component is not tested.

Another failure arises from Lighthouse’s emulatedFormFactor. When mobile is true, Lighthouse simulates a mobile viewport, but does not trigger touchstart events. A touch-friendly dropdown may pass all tests, but fails on real mobile devices. The solution is to run Lighthouse with --chrome-flags="--touch" and manually test on real devices.

pa11y often fails to validate dynamic content. A modal that loads data via fetch may not be audited if the pa11y-ci command runs before the data arrives. The fix is to add waitFor and waitForSelector to the pa11y-config.json:

{
  "waitFor": 3000,
  "waitForSelector": "#data-loading-spinner"
}

The waitFor ensures the page is fully loaded before running audits, and waitForSelector waits for a specific element to appear, such as a loading spinner.

A subtle but frequent mistake is mismatched aria-label and aria-labelledby. An icon button may have aria-label="Edit", but the label is a <span> that is not in the DOM when the screen reader starts. The label is missed. The solution is to ensure the label element is in the DOM before the button is announced.

Finalizing the Audit

The final artifact is a single report that combines automated results, manual checklists, and failure evidence. It is shared as a static HTML page generated from the audit results.

Use handlebars templates to generate the report:

<h1>Accessibility Audit: {{title}}</h1>
<ul>
  <li><strong>Status:</strong> {{status}}</li>
  <li><strong>Total Issues:</strong> {{totalIssues}}</li>
  <li><strong>Critical Issues:</strong> {{criticalIssues}}</li>
</ul>

<h2>Automated Results</h2>
<ul>
  {{#each tools}}
    <li><strong>{{name}}:</strong> {{results.total}} issues, {{results.critical}} critical</li>
  {{/each}}
</ul>

<h2>Manual Review</h2>
<h3>Screen Reader Navigation</h3>
<ul>
  {{#each screenReaderChecklist}}
    <li>{{#if checked}}✅{{else}}⬜{{/if}} {{description}}</li>
  {{/each}}
</ul>

<h2>Failure Evidence</h2>
{{#each failures}}
  <div class="failure">
    <h3>{{title}}</h3>
    <p><strong>Issue:</strong> {{issue}}</p>
    <p><strong>Expected:</strong> {{expected}}</p>
    <p><strong>Actual:</strong> {{actual}}</p>
    <img src="{{screenshotPath}}" alt="Screenshot of failure" />
  </div>
{{/each}}

Render this template with Handlebars.compile and fs.writeFileSync to produce audit-report.html.

The audit is complete when this report is reviewed, approved, and stored in a shared drive or version-controlled repository. It becomes the canonical reference for future audits, onboarding, and design reviews.

This page was rewritten on 14 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.