CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/axe-a11y

Automated accessibility scanning across the five engines - axe-core (primary), pa11y, Lighthouse a11y, WAVE, and IBM Equal Access. Authors and runs axe-core scans via the `axe.run()` JavaScript API or the @axe-core/playwright / @axe-core/cli wrappers, parses `violations[]` into per-rule severity, configures rule disable / disable-by-tag patterns, and emits CI-gateable output; references/ carry the pa11y CLI (htmlcs + axe runners), Lighthouse CI `categories:accessibility` assertions, the WAVE API / overlay, and IBM Equal Access (Section 508) with their verified CLI / API / config. Use for any automated a11y scanner setup - axe-core for JS/TS UI test suites on every PR, and the references for CLI-only, Lighthouse-pipeline, WebAIM-branded, or Section 508 scanning.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

lighthouse-a11y.mdreferences/

Lighthouse CI Accessibility category

Companion reference for axe-a11y. Consult when the project already runs Lighthouse CI for Web Vitals and wants a11y coverage in the same pipeline instead of a separate scanner. If the project doesn't already use Lighthouse CI, prefer direct axe integration (SKILL.md) - Lighthouse adds a layer.

Lighthouse CI ships five audit categories: Performance, Accessibility, Best Practices, SEO, and Progressive Web App. The Accessibility category runs a curated subset of axe-core rules (lhci). It audits whole pages (scored 0 - 1, rule pass rate weighted by severity); for component-level coverage, use axe in unit / integration tests.

Install and configure

npm install --save-dev @lhci/cli

Add a11y assertions to the same .lighthouserc.js used for perf, so one config drives both categories:

// .lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/', 'http://localhost:3000/checkout'],
      numberOfRuns: 3,
      startServerCommand: 'npm run start',
    },
    assert: {
      assertions: {
        // Accessibility - category score (0-1)
        'categories:accessibility':  ['error', { minScore: 0.95 }],

        // Per-audit overrides - error on critical-impact a11y rules
        'aria-required-attr':         ['error', { minScore: 1 }],
        'button-name':                ['error', { minScore: 1 }],
        'label':                      ['error', { minScore: 1 }],
        'meta-viewport':              ['error', { minScore: 1 }],

        // Lower-impact rules - warn but don't block
        'color-contrast':             ['warn',  { minScore: 1 }],
        'image-alt':                  ['warn',  { minScore: 1 }],
      },
    },
    upload: { target: 'temporary-public-storage' },
  },
};

Per lhci, assertion levels are 'error' (CI fails), 'warn' (surfaced but doesn't fail), and 'off'. Run all three phases (collect / assert / upload) with npx lhci autorun.

Per-URL thresholds with assertMatrix

assert.assertions applies one threshold set to every collected URL. When pages need different bars, use assertMatrix: an array pairing a matchingUrlPattern regex with its own assertions block (lhci). assertMatrix and assertions are mutually exclusive at the assert level, and the first matching pattern wins - order specific patterns before the catch-all:

assert: {
  assertMatrix: [
    { matchingUrlPattern: '.*/checkout.*',
      assertions: { 'categories:accessibility': ['error', { minScore: 0.98 }] } },
    { matchingUrlPattern: '.*',
      assertions: { 'categories:accessibility': ['error', { minScore: 0.90 }] } },
  ],
},

Common accessibility audit IDs

Used in assertions:; per lhci (full list in Lighthouse's accessibility audit documentation):

Audit IDWhat it checks
aria-allowed-attrARIA attributes are valid for the element's role.
aria-required-attrRequired ARIA attributes for the role are present.
aria-rolesValid ARIA roles only.
aria-valid-attr-valueARIA attribute values are valid.
button-nameButtons have accessible names.
bypassSkip-link or landmark for bypassing repeated content.
color-contrastForeground / background contrast >= 4.5:1 (3:1 large).
document-title<title> is set.
frame-title<iframe> has a title attribute.
html-has-lang<html> has lang.
image-alt<img> has alt.
labelForm fields have associated labels.
link-nameLinks have accessible names.
meta-viewport<meta name="viewport"> doesn't disable zoom.
tabindexNo tabindex > 0.

CI integration

# .github/workflows/lighthouse.yml
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm run build
      - run: npx lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
      - if: always()
        uses: actions/upload-artifact@v4
        with: { name: lighthouse-reports, path: .lighthouseci/ }

Anti-patterns and limits

  • Asserting only the category score hides per-rule regressions - assert specific audit IDs in addition to the score.
  • minScore: 1 on the category blocks every PR on a single moderate-severity failure; start at 0.95 and tighten.
  • A score of 1.0 doesn't mean perfect a11y - Lighthouse runs a subset of axe rules and covers ~50-60% of WCAG; pair with manual testing (screen-reader-test-author) and direct axe scans.
  • Page-level only - per-component coverage isn't supported.

References

SKILL.md

tile.json