axe-core vs Lighthouse CI for PR Gating
This decision guide is part of Pa11y CI Integration, and it answers a narrow, recurring question: when you can only put one accessibility engine on the blocking path of a pull request, should it be axe-core or Lighthouse CI? Both evaluate a rendered DOM against WCAG-aligned rules, but they differ sharply in how they report, how fast they run, and how much control they give you over the exit code that decides a merge. Picking the wrong one produces either a flaky gate developers learn to bypass or a gate so coarse it misses real regressions.
Baseline factors that decide the choice:
- Rule coverage and granularity per violation
- Wall-clock speed on the PR critical path
- Exit-code and threshold control for a clean pass/fail
- False-positive profile and how noise erodes trust
Why the Choice Matters
A PR gate is a trust contract. If it fires false alarms, engineers stop reading it and lobby to make it non-required; if it is too slow, it falls off the critical path and people merge before it finishes. axe-core and Lighthouse CI fail differently on both axes, so the choice is not cosmetic — it determines whether the gate actually holds.
axe-core reports discrete, rule-level violations (each tied to a WCAG criterion such as 1.4.3 or 4.1.2) and returns a clean non-zero exit on any violation, which makes “block this PR on serious and critical issues” trivial to express. Lighthouse CI instead computes a weighted accessibility category score; gating on a score threshold is coarser — a single new violation may not move the aggregate enough to fail, while unrelated audit changes can. Lighthouse also runs the full page and benefits from multiple runs to stabilize, so it is slower. The flip side: Lighthouse surfaces performance and best-practice signals in the same pass, which axe-core does not.
Comparison
| Factor | axe-core | Lighthouse CI |
|---|---|---|
| Output unit | Per-rule violations, each WCAG-mapped | Weighted category score (0–1) + per-audit detail |
| Granularity for gating | High — fail on any serious/critical rule | Coarse — score threshold can miss single violations |
| Speed on PR path | Fast — single render, single pass | Slower — full audit, multi-run for stability |
| Exit-code control | Exact — non-zero on violation, scriptable by severity | Score-based via assert thresholds in lighthouserc.js |
| False-positive profile | Low; conservative, well-targeted rules | Low–moderate; score can drift with browser/render |
| Scope beyond a11y | Accessibility only | Accessibility + performance + best practices + SEO |
| Best gate role | Primary blocking check on every PR | Secondary score guard on key pages |
| WCAG coverage style | Discrete criteria (e.g. SC 1.3.1, 1.4.3, 4.1.2) | Audit subset rolled into a category score |
Recommended Placement in the Gate
For most teams the cleanest split is to make axe-core the primary blocking check — fast, granular, and easy to gate by severity — and run Lighthouse CI as a secondary, often non-blocking, score guard on a handful of representative pages where the performance and best-practice signals add value. This keeps the blocking path quick and noise-free while still tracking the aggregate accessibility score over time.
# axe-core blocks; Lighthouse CI reports a score without blocking the merge.
jobs:
axe-gate: # primary, blocking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run build
- run: npx serve dist --listen 3000 &
- run: npx axe http://localhost:3000 --exit --tags wcag2aa # exit 1 blocks
lighthouse-score: # secondary, non-blocking signal
runs-on: ubuntu-latest
continue-on-error: true # records the score but does not block the merge
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx lhci autorun # asserts categories:accessibility minScore
If your gating policy needs branch-protection wiring and admin-bypass rules, align this split with Pull Request Gating & Branch Policies. To run both engines across many routes under one threshold, Pa11y CI integration wraps the axe engine for batch sweeps.
Edge Cases & Conditional Guards
- Score plateaus hide regressions: A Lighthouse accessibility score can stay flat while a new violation is introduced. Pin individual audits (
color-contrast,label) toerrorif you gate on Lighthouse, or keep axe-core as the hard block. - Single-page applications: Both engines need a stable, hydrated DOM. axe-core via a runner lets you
waitForLoadState('networkidle'); Lighthouse benefits from multiple runs to absorb timing noise. - Auth-gated routes: axe-core through Playwright can carry session state; Lighthouse needs a seeded preview or a login step before the audit.
Pipeline Impact
The two engines produce different artifacts: axe-core emits structured per-violation JSON ideal for PR annotation, while Lighthouse CI emits a category score plus an HTML report ideal for trend dashboards. Treat the axe-core exit code as the merge-blocking signal and the Lighthouse score as a tracked metric. Combining them with Progressive Threshold Management lets the blocking axe gate tighten over sprints while the Lighthouse score trends upward as evidence.
Common Pitfalls
- Gating solely on the Lighthouse score: A coarse threshold lets single violations slip through. Pair it with a per-rule check.
- Putting Lighthouse on the blocking path everywhere: Its slower, multi-run audit drags the PR critical path. Reserve it for key pages.
- Ignoring exit codes: Piping
|| trueafter axe-core defeats the gate. Let the non-zero exit propagate. - Comparing scores across browser versions: An unpinned browser shifts the Lighthouse score independent of code. Pin the browser if you gate on it.
FAQ
Which engine has better WCAG rule coverage for a blocking gate? axe-core exposes more discrete, individually gateable rules mapped to specific WCAG criteria, which makes “block on serious and critical” precise. Lighthouse evaluates an audit subset and folds it into a single weighted score, so it is better as a tracked signal than as the fine-grained blocker.
Can I just use Lighthouse CI for everything? You can, but the score-threshold model makes it easy to miss a single new violation that does not move the aggregate, and its multi-run audit is slower on the PR path. Most teams keep axe-core as the hard block and use Lighthouse as a secondary score guard.
Do these tools double-count the same violations? They overlap on common checks like color contrast (WCAG 2.2 SC 1.4.3) but express them differently — axe-core as a discrete rule, Lighthouse as a contributing audit. Run axe-core as the authority for pass/fail and treat the Lighthouse audit as corroborating signal rather than a second vote.
Related
- Pa11y CI Integration — the parent guide on orchestrating axe and htmlcs runners.
- Pull Request Gating & Branch Policies — wiring the chosen engine into branch protection.
- Lighthouse CI Baseline Configuration — configuring the score thresholds discussed here.