Testland
Browse all skills & agents

axe-a11y

Authors and runs axe-core accessibility scans - the most-deployed open-source a11y engine - via the `axe.run()` JavaScript API or the @axe-core/playwright / @axe-core/cli wrappers, parses the `violations[]` results into per-rule severity (critical / serious / moderate / minor), configures rule disable / disable-by-tag patterns, and emits JUnit-shaped output for CI gating. Use when the project ships UI tests in JavaScript / TypeScript and wants automated a11y coverage on every PR.

axe-a11y

Overview

axe-core is "an accessibility testing engine for websites and other HTML-based user interfaces" maintained by Deque (axe-core). It identifies approximately 57% of WCAG issues automatically and flags items requiring human review as "incomplete" (axe-core).

The integration shape: load the engine into a page (via test fixture, browser extension, or framework adapter), call axe.run(), and parse the violations[] array.

When to use

  • The project ships JavaScript / TypeScript UI tests (Playwright / Cypress / Jest / Vitest).
  • Automated a11y coverage on every PR is a goal.
  • The team values WCAG SC tagging - axe rules map cleanly to WCAG 2.0 / 2.1 / 2.2 SCs.
  • Pair with a11y-violation-gate for the ratchet pattern over baseline.

If the team is on a non-JS stack, evaluate pa11y-a11y (CLI; uses axe-core under the hood), lighthouse-a11y (CI-friendly, broader perf + a11y), or ibm-equal-access-a11y.

Install

npm install --save-dev axe-core

(Per axe-core.)

For framework integration (preferred over raw axe.run()):

PackageWhen to use
@axe-core/playwrightPlaywright tests.
@axe-core/reactReact-component scans during rendering.
@axe-core/cliHeadless CLI for arbitrary URLs.
axe-core/api/install (raw)Custom integrations.

Authoring scans

Raw axe.run() API

import axe from 'axe-core';

axe.run()
  .then(results => {
    if (results.violations.length) {
      console.error('a11y violations:', results.violations);
    }
  })
  .catch(err => {
    console.error('axe error:', err.message);
  });

(Adapted from axe-core.)

For test environments where loading axe via <script> is easier:

<script src="node_modules/axe-core/axe.min.js"></script>

Playwright integration

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

test('checkout page passes axe scan', async ({ page }) => {
  await page.goto('/checkout');

  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])     // limit to AA
    .analyze();

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

Cypress integration

// cypress/support/commands.js
import 'cypress-axe';

// In a test
cy.visit('/checkout');
cy.injectAxe();
cy.checkA11y();

Results structure

axe.run() resolves to an object with four arrays (axe-core):

FieldMeaning
violationsDefinite issues - block this in CI.
incompleteItems needing human review (axe couldn't determine).
passesSuccessful checks.
inapplicableRules that don't apply to this page.

Each violation has:

FieldMeaning
idRule ID (e.g. color-contrast, label, aria-required-attr).
impactcritical / serious / moderate / minor.
tagsIncludes wcag2a, wcag22aa, etc. - for severity-by-SC tagging.
descriptionOne-line explanation.
helpLonger remediation guidance.
helpUrlDirect link to Deque's rule documentation.
nodesArray of failing elements with target (selector), html, failureSummary.

Triage with jq:

# Top violations by impact
jq -r '.violations[] | "\(.impact): \(.id) — \(.description)"' axe-results.json

# Just the failing selectors per rule
jq -r '.violations[] | "\(.id):", (.nodes[].target | tostring)' axe-results.json

Rule configuration

By tags

axe ships rules tagged with conformance levels:

new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])

Common tag sets:

Tag setCoverage
['wcag2a', 'wcag2aa']WCAG 2.0/2.1 A + AA. Default for most teams.
['wcag2a', 'wcag2aa', 'wcag22aa']Adds WCAG 2.2 AA criteria.
['wcag2aaa']AAA-only (rarely the gate).
['best-practice']Non-WCAG good practices.
['experimental']Beta rules.

Disable specific rules

new AxeBuilder({ page }).disableRules(['color-contrast'])

For per-page disabling (e.g. a known false positive on a specific component):

new AxeBuilder({ page })
  .exclude('.legacy-component')   // selector exclusion
  .analyze();

For per-rule severity in CI gating (e.g. block on critical / serious only): handle in a11y-violation-gate using the impact field.

CI integration

# .github/workflows/a11y.yml
name: a11y

on:
  pull_request:
  push:
    branches: [main]

jobs:
  axe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npx playwright install --with-deps

      - name: Run a11y tests
        run: npx playwright test tests/a11y/

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: a11y-results
          path: playwright-report/
          retention-days: 14

For richer reporting, persist the raw accessibilityScanResults JSON as a build artifact and pipe to a11y-violation-gate.

Anti-patterns

Anti-patternWhy it failsFix
Asserting violations.length === 0Existing legacy debt blocks every PR.Use the ratchet pattern via a11y-violation-gate.
Disabling rules without comment in codeReviewer can't tell which rules are intentionally off vs. forgotten.Inline comment explaining why; quarterly review.
Scanning only the homepageMost a11y bugs hide in less-traveled flows.Scan a representative URL set: home + 1 logged-in dashboard + 1 form-heavy + 1 long-content.
Running axe in productionPerformance overhead; possible info leak via verbose error logging.CI / staging only.
Treating incomplete as passItems needing human review go unreviewed; defects escape.Track incomplete separately; manual review per quarter.
One mega-test that runs axe across every pageOne failure = whole test fails; remediation hard.One axe test per page; failure attribution clear.

Limitations

  • Catches ~57% of WCAG issues (axe-core) - the rest require manual screen-reader testing (per screen-reader-test-author).
  • Rule false positives. Rare but real; disableRules / exclude are escape hatches with documented rationale.
  • Doesn't cover dynamic state changes well. A modal that opens after user interaction won't be scanned unless the test triggers the open before calling axe.run().

References