How to check lighthouse accessibility score
Run the Lighthouse CLI with --only-categories=accessibility and read the number. It is the weighted average of the audits that applied to this page, and of nothing else. The order review page below scored 100 from 15 audits with weights adding to 112. example.com scored 96 from 10 adding to 68.
Why check this
Teams put this score in a release gate because it is one number anybody can read. Run it from the command line, on a URL the build produces, before the gate is written.
The number has a definition and it is narrow. Lighthouse runs axe-core, converts each rule result into an audit scored 0 or 1, multiplies by a fixed weight, and divides by the weights of the audits that applied. Audits with no matching element are dropped from both sides of that fraction. So the denominator is a property of the page, not of the standard, and two pages with the same score have been measured against different sets of audits.
The demo page below scores 100 with a broken tab order, a dangling label reference, a status message that cannot be announced and a link that says nothing. Lighthouse names two of those four defects in its own report, in a section that carries no weight.
Prerequisites
- Node 22 and Chrome installed locally.
npm i lighthousebrings the CLI and the bundled engine. The scoring document explains the weighted model, and each audit page in the accessibility reference names its rule. - The same lab as How to run an axe accessibility test: two pages on one port, one built to pass while broken and one with plain defects. Save it as
axe-demo.mjs.
// axe-demo.mjs - two pages on one port. node axe-demo.mjs
// / an order review page with four defects axe cannot see
// /broken/ the same page with four defects axe reports
import { createServer } from 'node:http';
const review = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Order review</title><style>
body{font:16px system-ui;color:#111;background:#fff;margin:2rem}
.row{display:flex;gap:.5rem}
#cancel{order:2} #continue{order:3} /* visual order, not DOM order */
</style></head><body>
<header><h1>Order review</h1></header>
<main>
<p>Two items, delivery on Friday.</p>
<div class="row"><button id="back">Back</button><button id="continue">Continue</button><button id="cancel">Cancel</button></div>
<p><a href="/terms-2026.pdf">Read more</a></p>
<button id="confirm" aria-labelledby="total-price">Confirm</button>
</main>
<footer><p>Returns accepted for 30 days.</p></footer>
<script>
document.getElementById('confirm').addEventListener('click', () => {
const s = document.createElement('div'); // live region created and
s.setAttribute('aria-live', 'polite'); // filled in the same frame,
s.textContent = 'Order placed.'; // so there is no change to
document.querySelector('main').appendChild(s); // announce
});
</script></body></html>`;
const broken = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Order review</title></head><body>
<header><h1>Order review</h1></header>
<main>
<img src="/box.png">
<input type="text" name="coupon">
<button id="apply"><svg width="16" height="16" aria-hidden="true"></svg></button>
<p style="color:#aaa;background:#fff">Delivery on Friday.</p>
</main>
<footer><p>Returns accepted for 30 days.</p></footer>
</body></html>`;
createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(req.url === '/broken/' ? broken : review);
}).listen(8477, () => console.log('http://127.0.0.1:8477/'));
- Figures below are one capture, on one machine, with Lighthouse 13.4.1 and Chrome 152.0.7977.76. Weights and the audit list change with the Lighthouse version, so read them from your own report rather than from any article.
Steps
- Step 1.
Start the lab in its own terminal.
node axe-demo.mjshttp://127.0.0.1:8477/ - Step 2.
Run the accessibility category on the order review page and keep the JSON report.
npx lighthouse http://127.0.0.1:8477/ --only-categories=accessibility --chrome-flags="--headless" --output=json --output-path=lhr-review.json… 2026-09-11T21:03:10.806Z LH:status Auditing: Custom controls have associated labels 2026-09-11T21:03:10.806Z LH:status Auditing: Custom controls have ARIA roles 2026-09-11T21:03:10.806Z LH:status Auditing: User focus is not accidentally trapped in a region 2026-09-11T21:03:10.807Z LH:status Auditing: Interactive controls are keyboard focusable 2026-09-11T21:03:10.807Z LH:status Auditing: Interactive elements indicate their purpose and state 2026-09-11T21:03:10.807Z LH:status Auditing: The page has a logical tab order 2026-09-11T21:03:10.807Z LH:status Auditing: The user's focus is directed to new content added to the page 2026-09-11T21:03:10.807Z LH:status Auditing: Offscreen content is hidden from assistive technology 2026-09-11T21:03:10.807Z LH:status Auditing: HTML5 landmark elements are used to improve navigation 2026-09-11T21:03:10.807Z LH:status Auditing: Visual order on the page follows DOM order … 2026-09-11T21:03:10.831Z LH:status Generating results... 2026-09-11T21:03:10.923Z LH:Printer json output written to lhr-review.jsonThe log says it audited logical tab order and visual order against DOM order. Step 4 shows what that means.
- Step 3.
Take the score apart. Save as
lh-read.mjs.// lh-read.mjs - node lh-read.mjs <lhr.json> what the accessibility score is made of import { readFileSync } from 'node:fs'; const lhr = JSON.parse(readFileSync(process.argv[2], 'utf8')); const refs = lhr.categories.accessibility.auditRefs; const mode = (r) => lhr.audits[r.id].scoreDisplayMode; const scored = refs.filter((r) => r.weight > 0 && lhr.audits[r.id].score !== null); const sum = (f) => scored.reduce((a, r) => a + f(r), 0); console.log(`lighthouse ${lhr.lighthouseVersion}, axe-core ${lhr.environment.credits['axe-core']}`); console.log(`${lhr.finalDisplayedUrl} score ${Math.round(lhr.categories.accessibility.score * 100)}`); console.log(`${refs.length} audits in the category: ${scored.length} scored and weighted, ` + `${refs.filter((r) => mode(r) === 'notApplicable').length} not applicable, ` + `${refs.filter((r) => mode(r) === 'manual').length} manual at weight 0`); for (const r of scored.sort((a, b) => b.weight - a.weight || a.id.localeCompare(b.id))) { console.log(` weight ${String(r.weight).padStart(2)} score ${lhr.audits[r.id].score} ${r.id}`); } console.log(`weighted average ${sum((r) => r.weight * lhr.audits[r.id].score)} / ${sum((r) => r.weight)}` + ` = ${(sum((r) => r.weight * lhr.audits[r.id].score) / sum((r) => r.weight)).toFixed(4)}`);node lh-read.mjs lhr-review.jsonlighthouse 13.4.1, axe-core 4.13.0 http://127.0.0.1:8477/ score 100 76 audits in the category: 15 scored and weighted, 51 not applicable, 10 manual at weight 0 weight 10 score 1 aria-allowed-attr weight 10 score 1 aria-hidden-body weight 10 score 1 aria-valid-attr weight 10 score 1 aria-valid-attr-value weight 10 score 1 button-name weight 7 score 1 aria-conditional-attr weight 7 score 1 aria-prohibited-attr weight 7 score 1 color-contrast weight 7 score 1 document-title weight 7 score 1 html-has-lang weight 7 score 1 html-lang-valid weight 7 score 1 link-name weight 7 score 1 target-size weight 3 score 1 heading-order weight 3 score 1 landmark-one-main weighted average 112 / 112 = 1.0000Fifteen audits out of 76 produced the 100. Weights are 10, 7 and 3, so
button-nameis worth more than three timeslandmark-one-main, andhtml-lang-validandcolor-contrastare worth the same. - Step 4.
List the audits Lighthouse ran but did not score.
node -e "const l=require('./lhr-review.json');l.categories.accessibility.auditRefs.filter(r=>l.audits[r.id].scoreDisplayMode==='manual').forEach(r=>console.log('weight '+r.weight+' '+l.audits[r.id].title))"weight 0 Interactive controls are keyboard focusable weight 0 Interactive elements indicate their purpose and state weight 0 The page has a logical tab order weight 0 Visual order on the page follows DOM order weight 0 User focus is not accidentally trapped in a region weight 0 The user's focus is directed to new content added to the page weight 0 HTML5 landmark elements are used to improve navigation weight 0 Offscreen content is hidden from assistive technology weight 0 Custom controls have associated labels weight 0 Custom controls have ARIA rolesThree of these audits describe this page. Its visual order does not follow DOM order, and its tab order is not logical (How to check focus order). Focus is never directed to the content the Confirm button adds, and that content is a live region nothing can announce (How to test aria live regions). All three sit at weight 0 under Additional items to manually check, and the score stayed at 100.
- Step 5.
Score the page with ordinary defects and watch the denominator move.
npx lighthouse http://127.0.0.1:8477/broken/ --only-categories=accessibility --chrome-flags="--headless" --output=json --output-path=lhr-broken.json --quiet && node lh-read.mjs lhr-broken.jsonlighthouse 13.4.1, axe-core 4.13.0 http://127.0.0.1:8477/broken/ score 58 76 audits in the category: 12 scored and weighted, 54 not applicable, 10 manual at weight 0 weight 10 score 1 aria-hidden-body weight 10 score 0 button-name weight 10 score 0 image-alt weight 10 score 0 label weight 7 score 1 aria-hidden-focus weight 7 score 0 color-contrast weight 7 score 1 document-title weight 7 score 1 html-has-lang weight 7 score 1 html-lang-valid weight 7 score 1 target-size weight 3 score 1 heading-order weight 3 score 1 landmark-one-main weighted average 51 / 88 = 0.5795Four failures, 51 of 88 weight left, and 0.5795 rounds to 58. Twelve audits carried this score against fifteen for the page before it.
- Step 6.
Score a page with none of the planted defects.
npx lighthouse https://example.com/ --only-categories=accessibility --chrome-flags="--headless" --output=json --output-path=lhr-example.json --quiet && node lh-read.mjs lhr-example.jsonlighthouse 13.4.1, axe-core 4.13.0 https://example.com/ score 96 76 audits in the category: 10 scored and weighted, 56 not applicable, 10 manual at weight 0 weight 10 score 1 aria-hidden-body weight 10 score 1 meta-viewport weight 7 score 1 color-contrast weight 7 score 1 document-title weight 7 score 1 html-has-lang weight 7 score 1 html-lang-valid weight 7 score 1 link-name weight 7 score 1 target-size weight 3 score 1 heading-order weight 3 score 0 landmark-one-main weighted average 65 / 68 = 0.9559One missing main landmark costs 3 of 68 and takes the page to 96. The demo page, which a keyboard user cannot cancel, scores 100. Ranking the two by score gets the answer backwards.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| A score of 100 | Every weighted audit that applied returned 1 | Read the manual list and run those checks. The score covered none of them. |
| 15 scored and weighted | The size of the denominator on this run | Quote it next to the score. Without it the score is not comparable to another page. |
| 51 not applicable | 51 audits found no matching element | A page with fewer components has fewer applicable audits and a smaller denominator. |
| 10 manual at weight 0 | Lighthouse named these and scored none | Assign them. Each maps to a procedure below. |
| A failing audit at weight 3 | It moves the score less than one at weight 10 | Fix by severity for users, not by score impact. |
Common mistakes
What to check next
- How to run an axe accessibility test: the engine under the score, with the four defects it reported nothing about.
- How to test keyboard navigation on a website: covers two of the manual audits, keyboard focusable controls and focus traps.
- How to check focus order: the manual audit named visual order follows DOM order, measured instead of assumed.
- How to check tap target size:
target-sizescored 1 at weight 7 on every run above, which is worth confirming by hand. - How to check color contrast ratio: the audit most likely to flip between runs when a background is an image.
FAQ
What is a good lighthouse accessibility score?
A useful one is 100 plus a completed manual list. Anything under 100 means a rule with a mechanical answer failed, so fix those first. Reaching 100 means the weighted audits that applied passed, which the page above does while broken.
How do I check the lighthouse score without the CLI?
Open DevTools, the Lighthouse panel, tick Accessibility and press Analyze page load. The panel uses the same audits and weights. The CLI is here because its JSON report holds the weights, while the panel shows a gauge.
Why did the score change when nothing changed?
The audit set moves with the page. Content that renders late, a cookie banner that appears once, or an image that fails to load can make an audit applicable or not, which changes the denominator and the score.
Is the accessibility score part of Core Web Vitals?
No. Core Web Vitals are field performance metrics. The accessibility category is a separate lab audit.
Does a score of 100 mean the page meets WCAG?
No. Ten audits in the category are marked manual at weight 0, and three of them describe defects the 100-scoring page has.
Verified
Verified by Maks Vernylighthouse 13.4.1axe-core 4.13.0 (bundled)Chrome 152.0.7977.76node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate10 minpublished updated Maks Verny