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.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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.
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.
npm install --save-dev axe-core(Per axe-core.)
For framework integration (preferred over raw axe.run()):
| Package | When to use |
|---|---|
@axe-core/playwright | Playwright tests. |
@axe-core/react | React-component scans during rendering. |
@axe-core/cli | Headless CLI for arbitrary URLs. |
axe-core/api/install (raw) | Custom integrations. |
axe.run() APIimport 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>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/support/commands.js
import 'cypress-axe';
// In a test
cy.visit('/checkout');
cy.injectAxe();
cy.checkA11y();axe-core plus the framework wrapper (@axe-core/playwright
for Playwright; cypress-axe for Cypress)..withTags([...]) (e.g. wcag2a,
wcag2aa, wcag22aa), then .analyze()..disableRules([...])
or .exclude(selector), each with an inline reason.accessibilityScanResults JSON as a CI artifact.a11y-violation-gate for the ratchet gate instead
of asserting violations.length === 0.incomplete items to periodic manual screen-reader review
(per screen-reader-test-author).axe.run() resolves to violations / incomplete / passes /
inapplicable arrays; each violation carries id, impact, tags,
and a nodes[] array of failing selectors. Rules are selected by WCAG
tag (withTags) and suppressed by rule id or selector (disableRules
/ exclude). Full field tables, tag sets, and jq triage:
references/results-and-config.md.
A Playwright suite adds checkout.a11y.spec.ts. The test navigates to
/checkout, runs
new AxeBuilder({ page }).withTags(['wcag2a','wcag2aa','wcag22aa']).analyze(),
and writes the result JSON to axe-results.json.
The run returns one entry in violations[]: id: color-contrast,
impact: serious, tags including wcag2aa, and one node targeting
button.primary with a failureSummary.
jq -r '.violations[] | "\(.impact): \(.id)"' axe-results.json prints
serious: color-contrast. Piping the JSON to a11y-violation-gate
compares it against the baseline: because this rule/selector pair is
new, the gate fails the PR with the failing selector attributed. Fixing
the button's foreground colour clears it, and the next run reports
violations: [].
# .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: 14For richer reporting, persist the raw accessibilityScanResults
JSON as a build artifact and pipe to
a11y-violation-gate.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Asserting violations.length === 0 | Existing legacy debt blocks every PR. | Use the ratchet pattern via a11y-violation-gate. |
| Disabling rules without comment in code | Reviewer can't tell which rules are intentionally off vs. forgotten. | Inline comment explaining why; quarterly review. |
| Scanning only the homepage | Most 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 production | Performance overhead; possible info leak via verbose error logging. | CI / staging only. |
Treating incomplete as pass | Items needing human review go unreviewed; defects escape. | Track incomplete separately; manual review per quarter. |
| One mega-test that runs axe across every page | One failure = whole test fails; remediation hard. | One axe test per page; failure attribution clear. |
screen-reader-test-author).disableRules /
exclude are escape hatches with documented rationale.axe.run().axe.run(), results
structure.a11y-violation-gate -
CI gate using axe results.pa11y-a11y,
lighthouse-a11y,
wave-a11y,
ibm-equal-access-a11y -
alternative scanners.