Lighthouse CI Accessibility Budgets

Permalink to "Lighthouse CI Accessibility Budgets"

Lighthouse’s accessibility category runs a subset of axe-core audits and produces a weighted score from 0 to 100. Lighthouse CI (@lhci/cli) runs it on every build and can fail CI when the score or specific audits regress. It is easy to adopt and gives product owners a number they understand. It is also easy to misuse: a score of 100 means the page passed the audits Lighthouse runs, on the state it loaded — not that it conforms to WCAG — and a score threshold alone lets individual regressions through if other audits improve.

This page configures Lighthouse CI as a regression budget for data pages, with assertions per audit. It belongs to accessibility testing recipes.

Spec reference

Permalink to "Spec reference"

Lighthouse CI configuration (lighthouserc.js):

  • ci.collect.url — URLs to audit; ci.collect.puppeteerScript can log in or set up state; numberOfRuns for stability.
  • ci.assert.assertions — per-category (categories:accessibility) and per-audit (button-name, color-contrast, td-headers-attr, aria-required-children, …) assertions with levels error/warn and options like minScore.
  • ci.upload — to temporary public storage, an LHCI server, or the filesystem.

Lighthouse’s accessibility audits are axe-core rules; their weights determine the score. Audits that do not apply to a page are excluded from the score, so a page with no tables cannot fail table audits.

Score threshold versus per-audit assertions Comparison of asserting only on the Lighthouse accessibility category score against asserting that each relevant audit passes. Score threshold versus per-audit assertions✗ Score onlyOne regression hidden by another fixNo signal about which audit changedEncourages chasing the number✓ Per-audit assertionsEach relevant audit must passAny regression fails with its audit nameScore kept as a trend, ratchetedDirectly actionable failures
A score can stay at 96 while a table loses its headers — per-audit assertions catch that.

When Lighthouse CI fits — and when it does not

Permalink to "When Lighthouse CI fits — and when it does not"

It fits as a broad, cheap regression net across many pages, especially if Lighthouse CI already runs for performance. The same run yields accessibility results at no extra cost.

It does not fit as the primary accessibility test for data UIs. It audits the page as loaded (unless scripted), runs fewer rules than a full axe scan with WCAG tags, and cannot see keyboard behaviour or announcements. Use it alongside axe scans of grid states with Playwright and keyboard tests.

The misapplication to name is reporting “Lighthouse accessibility: 100” as evidence of WCAG conformance in procurement or compliance documents. It is evidence that a set of automated checks passed on specific pages.

Annotated code example

Permalink to "Annotated code example"
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: [
        'http://localhost:8080/invoices',
        'http://localhost:8080/invoices?sort=amount&dir=asc',   // a state reachable by URL
        'http://localhost:8080/reports/revenue',
      ],
      numberOfRuns: 1,                                          // accessibility is deterministic
      settings: { onlyCategories: ['accessibility'] },          // fast, if perf runs elsewhere
    },
    assert: {
      assertions: {
        // Trend floor: current level, raised as you fix things
        'categories:accessibility': ['error', { minScore: 0.95 }],
        // Hard gates on the audits that matter for data UIs
        'button-name': 'error',
        'link-name': 'error',
        'color-contrast': 'error',
        'td-headers-attr': 'error',
        'th-has-data-cells': 'error',
        'aria-required-children': 'error',
        'aria-required-parent': 'error',
        'aria-allowed-attr': 'error',
        'duplicate-id-aria': 'error',
        'label': 'error',
        'heading-order': 'warn',
      },
    },
    upload: { target: 'filesystem', outputDir: './lhci-reports' },
  },
};
# CI step
- run: npm run build && npx http-server dist -p 8080 &
- run: npx wait-on http://localhost:8080
- run: npx @lhci/cli autorun
- uses: actions/upload-artifact@v4
  with: { name: lighthouse, path: lhci-reports }

URL-reachable states (a sort in the query string, a filter parameter) are the cheapest way to extend Lighthouse beyond the initial load. For states that need interaction, puppeteerScript can perform it, but at that point a Playwright test with axe is usually simpler.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"

Lighthouse includes a short list of “manual” items in its report (logical tab order, focus visibility, custom controls) that it does not test. Treat them as a reminder list, not as checked items.

Lighthouse report item Tested automatically? Where to test it
Buttons have names ✓ —
Table headers valid ✓ —
Contrast (text) ✓ —
Tab order is logical ✗ (manual list) Keyboard tests, manual audit
Focus visible ✗ (manual list) Focus screenshot tests
Custom controls have roles ✗ (manual list) Role queries, screen reader tests
A budget ratchet over releases Bar chart of an example accessibility score floor raised release by release as issues are fixed, with per-audit gates in place throughout. A budget ratchet over releasesRelease 1 — baseline82 minimum scoreRelease 288 minimum scoreRelease 393 minimum scoreRelease 497 minimum score
An example ratchet: the floor only moves up, and each raise follows fixes rather than exceptions.

Integration context

Permalink to "Integration context"

Lighthouse and pa11y overlap; pick one as the page-level net rather than running both for the same purpose — see pa11y-ci for multi-state data pages. If Lighthouse CI already runs for performance, the DOM and accessibility-tree budgets from setting a DOM node budget in CI can live in the same configuration.

The rule decisions behind Lighthouse’s audits are axe-core’s; suppressions should follow the ledger in configuring axe-core rules and handling false positives.

Adopting the budget without blocking everyone Steps to introduce Lighthouse CI accessibility budgets: measure a baseline, gate the critical audits immediately, set the score floor at the baseline, and ratchet it with fixes. Adopting the budget without blocking everyoneBaselinerun once; record scores and failing auditsper URLGate critical auditsbutton-name, table headers, contrast as errorsimmediatelyFloor at baselinecategories:accessibility minScore = todayno regressionsRatchetraise the floor after each batch of fixesnever lower it
Gate the audits that matter from day one; let the score floor follow the fixes.

Gotchas

Permalink to "Gotchas"

Score variance. Accessibility scores are deterministic for the same DOM, but pages with random or time-based content vary. Seed data for CI.

Not-applicable audits. A page whose table fails to render has no table audits, and may score higher. Assert on content presence (a row count) in a separate test.

Authenticated pages. Use puppeteerScript to log in, or run against a seeded, unauthenticated preview build.

Design system notes

Permalink to "Design system notes"

Run Lighthouse CI against the design system’s documentation site as a broad smoke check, but rely on per-story axe scans for components. For products, provide a shared lighthouserc preset with the data-UI audit gates above, so every product’s budget starts from the same list.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
Does a Lighthouse accessibility score of 100 mean a page is WCAG compliant?

No. It means the page passed the automated audits Lighthouse runs, in the state it was loaded. Keyboard behaviour, announcements, focus and many WCAG criteria are not tested.

Should Lighthouse CI fail on the accessibility score or on individual audits?

On individual audits for the issues that matter, with the category score as a floor that only goes up. A score threshold alone can hide a regression behind an unrelated improvement.

Can Lighthouse test interactive states of a data grid?

Only states reachable by URL or scripted with a Puppeteer script. For interactive states, Playwright or Cypress tests with axe are simpler and more thorough.

Does Lighthouse run the same rules as a full axe-core scan?

No. It runs a curated subset of axe-core audits with its own weighting. A full axe scan with WCAG tags checks more rules, which is why Lighthouse works best as a broad regression net rather than the main accessibility test.

How should the score floor be set initially?

At the current baseline, so nothing gets worse, with critical audits gated immediately. Raise the floor as fixes land.

Permalink to "Related"

← Back to Accessibility Testing Recipes