Accessibility Testing Tools Cheat Sheet
CLI and code snippets for axe-core, Lighthouse, Pa11y, and jest-axe to automate WCAG accessibility checks in development and CI.
axe-core in the Browser Console
Run a full accessibility audit against the current page without any build step.
// paste into devtools console, or load via CDN script tagaxe.run().then((results) => { console.log(`${results.violations.length} violations found`) results.violations.forEach((v) => { console.log(v.id, v.impact, v.help, v.nodes.map(n => n.target)) })})
jest-axe Unit Test
Assert a rendered component has zero automatically-detectable violations.
import { render } from '@testing-library/react'import { axe, toHaveNoViolations } from 'jest-axe'import { LoginForm } from './LoginForm'expect.extend(toHaveNoViolations)test('LoginForm has no accessibility violations', async () => { const { container } = render(<LoginForm />) const results = await axe(container) expect(results).toHaveNoViolations()})
Pa11y CLI for CI
Command-line accessibility testing against a live URL, ideal for CI pipelines.
# single page audit against WCAG2AAnpx pa11y https://example.com --standard WCAG2AA# crawl a sitemap and fail CI on any errornpx pa11y-ci --sitemap https://example.com/sitemap.xml# pa11y.json config# {# "defaults": { "standard": "WCAG2AA", "timeout": 30000 },# "urls": ["https://example.com/", "https://example.com/pricing"]# }
Lighthouse CI Accessibility Budget
Enforce a minimum accessibility score as part of a CI gate.
# lighthouserc.ymlci: collect: url: - https://staging.example.com/ - https://staging.example.com/checkout assert: assertions: "categories:accessibility": - error - minScore: 0.95 upload: target: temporary-public-storage
Common axe-core Rule IDs You'll See
Frequently flagged violations and what they mean.
- color-contrast- text/background contrast ratio falls below WCAG AA (4.5:1 normal text)
- image-alt- <img> missing a meaningful alt attribute
- label- form input has no associated <label> or aria-label
- aria-required-attr- an element with an ARIA role is missing a required ARIA attribute
- region- page content isn't contained within a landmark region (main, nav, header...)
- duplicate-id-aria- an id referenced by aria-* attributes is duplicated on the page
Playwright + axe-core E2E Audit
Run a full-page accessibility scan inside a real Playwright test and fail on any violation.
import { test, expect } from '@playwright/test'import AxeBuilder from '@axe-core/playwright'test('checkout page has no a11y violations', async ({ page }) => { await page.goto('/checkout') const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag22aa']) .exclude('#third-party-widget') .analyze() expect(results.violations, JSON.stringify(results.violations, null, 2)).toEqual([])})
cypress-axe Custom Command
Inject axe into a Cypress run and assert on violations after interacting with the page.
// cypress/support/e2e.jsimport 'cypress-axe'// cypress/e2e/nav.cy.jsdescribe('main navigation', () => { it('is accessible after opening the mega menu', () => { cy.visit('/') cy.injectAxe() cy.get('[data-testid="nav-learn"]').click() cy.checkA11y('#mega-menu-panel', { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] }, }, (violations) => { cy.task('log', `${violations.length} violations`) }) })})
Configuring & Disabling axe Rules
Scope a scan to specific WCAG tags or silence a rule that's a known false positive for your design system.
axe.configure({ rules: [ // treat a custom component's role mapping as valid { id: 'aria-allowed-role', selector: '.ds-icon-button', enabled: false }, ],})axe .run(document, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'best-practice'] }, resultTypes: ['violations'], }) .then((results) => { // filter to only 'critical' and 'serious' before failing a build gate const blocking = results.violations.filter((v) => ['critical', 'serious'].includes(v.impact)) if (blocking.length) throw new Error(`${blocking.length} blocking a11y violations`) })
GitHub Actions: Fail PR Below an A11y Score
Gate merges on pa11y-ci exit code plus an uploaded JSON report artifact for reviewers.
name: accessibilityon: pull_requestjobs: a11y: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npm run build && npm run start & npx wait-on http://localhost:3000 - name: Run pa11y-ci run: npx pa11y-ci --json > pa11y-report.json - name: Upload report if: always() uses: actions/upload-artifact@v4 with: name: pa11y-report path: pa11y-report.json
Beyond the Basics: Terms You'll Hit at Scale
Concepts that come up once you move past single-page automated scans.
- Accessible Name Computation- the browser algorithm (label, aria-label, aria-labelledby, alt, title, content) that decides an element's announced name; axe flags conflicts but won't tell you which source wins
- Shadow DOM piercing- axe-core traverses open shadow roots automatically, but closed shadow roots (some Web Components) are invisible to it entirely
- Impact severity- axe classifies each violation as critical, serious, moderate, or minor; CI gates typically block on critical+serious only
- Best-practice vs WCAG tags- rules tagged 'best-practice' aren't required for WCAG conformance but catch real UX problems (e.g. duplicate landmark labels)
- Focus order testing- automated tools can't verify logical tab order; requires a scripted keyboard walk (Playwright's page.keyboard.press('Tab')) asserting focus targets in sequence
- ARIA live region verbosity- axe checks live regions exist but not whether screen readers announce them at the right verbosity (polite vs assertive) — needs manual NVDA/VoiceOver verification
Automated tools like axe-core only catch roughly 30-40% of WCAG success criteria (contrast, missing labels, ARIA misuse) — always pair CI-gated axe/Pa11y checks with manual keyboard-only and screen-reader (NVDA/VoiceOver) passes before calling a flow accessible.