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

// 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/'));

Steps

  1. Step 1.

    Start the lab in its own terminal.

    node axe-demo.mjs
    
    http://127.0.0.1:8477/
  2. 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.json

    The log says it audited logical tab order and visual order against DOM order. Step 4 shows what that means.

  3. 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.json
    
    lighthouse 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.0000

    Fifteen audits out of 76 produced the 100. Weights are 10, 7 and 3, so button-name is worth more than three times landmark-one-main, and html-lang-valid and color-contrast are worth the same.

  4. 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 roles

    Three 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.

  5. 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.json
    
    lighthouse 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.5795

    Four failures, 51 of 88 weight left, and 0.5795 rounds to 58. Twelve audits carried this score against fifteen for the page before it.

  6. 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.json
    
    lighthouse 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.9559

    One 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

Sign: The score is treated as a percentage of accessibility problems solved.Cause: It is a weighted average of binary audits, with weights of 10, 7, 3 or 1, over a denominator that changes with the page. On the runs above the denominator was 112, then 88, then 68, for three pages measured by the same tool on the same afternoon.
Sign: Two pages are ranked against each other by their scores.Cause: The order review page scored 100 with a tab order that contradicts the layout and a status nobody hears. example.com scored 96, its only failure being a missing main landmark. The scores measure different audit sets, so the comparison has no meaning.
Sign: Lighthouse scores an audit as a pass that axe-core, run alone, refused to decide.Cause: Lighthouse 13.4.1 bundles axe-core 4.13.0. On this page axe alone put aria-valid-attr-value in the incomplete bucket, with the Confirm button quoted. In the Lighthouse report the same audit carries score 1 and no items, at weight 10. Same engine, same DOM, opposite readings.

What to check next

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.

intermediate10 minpublished updated Maks Verny