[{"data":1,"prerenderedAt":2138},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets\u002F":314,"content-navigation":1986},[4,84,88,216],{"title":5,"path":6,"stem":7,"children":8},"Core Accessibility Principles For Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks","core-accessibility-principles-for-modern-frameworks",[9,12,18,24,36,48,66,78],{"title":10,"path":6,"stem":11},"Core Accessibility Principles for Modern Frameworks","core-accessibility-principles-for-modern-frameworks\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Accessible Color Contrast & Theming","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming","core-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002Findex",[17],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Accessible Form Validation & Error States","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states","core-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Focus Management Strategies for SPAs","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Findex",[29,30],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Handling Focus Restoration After Dynamic Route Changes","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Keyboard Navigation Patterns for Modals","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Findex",[41,42],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"Building Accessible Dropdowns Without External UI Kits","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits\u002Findex",[47],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Reduced Motion & Animation Accessibility","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Findex",[53,54,60],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Accessible Loading Skeletons and Spinners","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Respecting prefers-reduced-motion in React and CSS","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Screen Reader Compatibility Testing","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Findex",[71,72],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Testing ARIA Live Regions with Jest and Testing Library","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"Semantic HTML vs ARIA in Component Trees","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees","core-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002Findex",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},"Modern Framework Accessibility","\u002F","index",{"title":89,"path":90,"stem":91,"children":92},"React Nextjs Accessibility Patterns","\u002Freact-nextjs-accessibility-patterns","react-nextjs-accessibility-patterns",[93,96,108,132,156,162,180,204],{"title":94,"path":90,"stem":95},"React & Next.js Accessibility Patterns","react-nextjs-accessibility-patterns\u002Findex",{"title":97,"path":98,"stem":99,"children":100},"Accessible Component Libraries in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Findex",[101,102],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Building Accessible Tabs in React Without Radix UI","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"Accessible Data Tables & Grids in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Findex",[113,114,120,126],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Accessible Pagination for React Data Tables","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002Findex",[119],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Building a Sortable Accessible Data Table in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002Findex",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"Virtualizing Long Lists Accessibly in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002Findex",[131],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"Dynamic Content & State Announcements","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Findex",[137,138,144,150],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Accessible Toast Notifications in React","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Faccessible-toast-notifications-in-react","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Faccessible-toast-notifications-in-react\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Announcing Client-Side Route Changes in React","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Fannouncing-client-side-route-changes-in-react","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Fannouncing-client-side-route-changes-in-react\u002Findex",[149],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"React Context for Accessibility Preferences","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences\u002Findex",[155],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":160},"Form Handling with React Hook Form & A11y","\u002Freact-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y","react-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y\u002Findex",[161],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":166},"Next.js App Router Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Findex",[167,168,174],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":172},"Skip Links in Next.js App Router","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router\u002Findex",[173],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":178},"Next.js Dynamic Imports & Keyboard Navigation","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation\u002Findex",[179],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":184},"React Hooks for Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Findex",[185,186,192,198],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":190},"Building a useAnnouncer Hook for Live Regions","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions\u002Findex",[191],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":196},"Fixing Focus Trap Issues in React Portals","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002Findex",[197],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":202},"Making React useEffect Accessible for Screen Readers","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers\u002Findex",[203],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":208},"Server Components & Client-Side Interactivity","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex",[209,210],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":214},"Accessible Modals in Next.js 14 Server Components","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002Findex",[215],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":220},"Testing And Automating Accessibility","\u002Ftesting-and-automating-accessibility","testing-and-automating-accessibility",[221,224,242,260,278,296],{"title":222,"path":218,"stem":223},"Testing & Automating Accessibility","testing-and-automating-accessibility\u002Findex",{"title":225,"path":226,"stem":227,"children":228},"Accessibility Audits with Lighthouse","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Findex",[229,230,236],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":234},"Interpreting Lighthouse Accessibility Scores","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores\u002Findex",[235],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":240},"Setting Lighthouse CI Accessibility Budgets","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets\u002Findex",[241],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":246},"Automated Accessibility Testing with axe-core","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Findex",[247,248,254],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":252},"Catching Color Contrast Failures with axe-core","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core\u002Findex",[253],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":258},"Writing Custom axe-core Rules","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules\u002Findex",[259],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":264},"Component Testing with jest-axe","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Findex",[265,266,272],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":270},"Debugging jest-axe Violations in CI","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci\u002Findex",[271],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":276},"Testing React Components with jest-axe","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe\u002Findex",[277],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":282},"End-to-End Accessibility Testing with Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Findex",[283,284,290],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":288},"Asserting Focus Order in Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fasserting-focus-order-in-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fasserting-focus-order-in-playwright\u002Findex",[289],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":294},"Keyboard Navigation Tests in Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fkeyboard-navigation-tests-in-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fkeyboard-navigation-tests-in-playwright\u002Findex",[295],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":300},"Gating Accessibility in CI\u002FCD Pipelines","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Findex",[301,302,308],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":306},"Accessibility Regression Testing in GitHub Actions","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions\u002Findex",[307],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":312},"Failing Pull Requests on axe Violations","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations\u002Findex",[313],{"title":309,"path":310,"stem":311},{"id":315,"title":237,"body":316,"date":1979,"description":1980,"extension":1981,"image":1979,"meta":1982,"modifiedAt":1979,"navigation":656,"noindex":1983,"path":238,"publishedAt":1979,"seo":1984,"stem":239,"updatedAt":1979,"__hash__":1985},"content\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets\u002Findex.md",{"type":317,"value":318,"toc":1966},"minimark",[319,323,345,351,405,408,413,416,464,472,474,478,505,842,866,881,883,887,898,1069,1080,1082,1086,1098,1341,1354,1361,1363,1367,1370,1391,1400,1449,1460,1462,1466,1473,1484,1654,1667,1677,1679,1683,1686,1692,1722,1738,1743,1745,1749,1809,1811,1815,1829,1831,1835,1861,1885,1905,1918,1942,1944,1948,1962],[320,321,237],"h1",{"id":322},"setting-lighthouse-ci-accessibility-budgets",[324,325,326,327,331,332,335,336,339,340,344],"p",{},"A passing Lighthouse audit today does nothing to stop a contrast regression from shipping tomorrow. The only durable way to hold an accessibility baseline is to make a drop below it fail the build. Lighthouse CI (",[328,329,330],"code",{},"@lhci\u002Fcli",") does exactly that: it runs Lighthouse in your pipeline, asserts your accessibility score and individual audits against declared thresholds, and exits non-zero when they slip—failing the pull request before the regression merges. This guide configures ",[328,333,334],{},"lighthouserc"," assertions and budgets, wires ",[328,337,338],{},"lhci autorun"," into GitHub Actions, fails the build on regression, and shows how to allowlist known debt without disabling the gate. It is the enforcement half of the ",[341,342,225],"a",{"href":343},"\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002F",".",[324,346,347],{},[348,349,350],"strong",{},"Mapped WCAG 2.1\u002F2.2 Success Criteria:",[352,353,354,369,383,392],"ul",{},[355,356,357,360,361,364,365,368],"li",{},[328,358,359],{},"1.4.3 Contrast (Minimum)"," – The ",[328,362,363],{},"color-contrast"," audit, commonly promoted to a hard ",[328,366,367],{},"error"," in CI.",[355,370,371,374,375,378,379,382],{},[328,372,373],{},"4.1.2 Name, Role, Value"," – Accessible-name audits (",[328,376,377],{},"button-name",", ",[328,380,381],{},"link-name",") gated as errors.",[355,384,385,360,388,391],{},[328,386,387],{},"1.1.1 Non-text Content",[328,389,390],{},"image-alt"," audit enforced on every build.",[355,393,394,397,398,378,401,404],{},[328,395,396],{},"1.3.1 Info and Relationships"," – Structural audits (",[328,399,400],{},"heading-order",[328,402,403],{},"list",") asserted to prevent drift.",[406,407],"hr",{},[409,410,412],"h2",{"id":411},"prerequisites","Prerequisites",[324,414,415],{},"You need a buildable app that serves a real route, Node 18+, and the Lighthouse CI package. Install it as a dev dependency so the pinned version is reproducible across local runs and CI:",[417,418,423],"pre",{"className":419,"code":420,"language":421,"meta":422,"style":422},"language-bash shiki shiki-themes github-light github-dark","npm install --save-dev @lhci\u002Fcli\n# Sanity-check the binary\nnpx lhci --version\n","bash","",[328,424,425,445,452],{"__ignoreMap":422},[426,427,430,434,438,442],"span",{"class":428,"line":429},"line",1,[426,431,433],{"class":432},"sScJk","npm",[426,435,437],{"class":436},"sZZnC"," install",[426,439,441],{"class":440},"sj4cs"," --save-dev",[426,443,444],{"class":436}," @lhci\u002Fcli\n",[426,446,448],{"class":428,"line":447},2,[426,449,451],{"class":450},"sJ8bj","# Sanity-check the binary\n",[426,453,455,458,461],{"class":428,"line":454},3,[426,456,457],{"class":432},"npx",[426,459,460],{"class":436}," lhci",[426,462,463],{"class":440}," --version\n",[324,465,466,467,471],{},"Decide your threshold before writing config. A common pattern is to assert the ",[468,469,470],"em",{},"current"," score as the floor (so you never regress) rather than aiming for an aspirational number you don't yet meet—then ratchet the floor up as you fix debt.",[406,473],{},[409,475,477],{"id":476},"the-lighthouserc-config-assertassertions","The lighthouserc Config: assert.assertions",[324,479,480,481,484,485,488,489,378,492,378,495,497,498,501,502,504],{},"Lighthouse CI's enforcement lives in ",[328,482,483],{},"assert.assertions",". Each entry maps an audit ID—or the special ",[328,486,487],{},"categories:accessibility"," key—to a severity (",[328,490,491],{},"off",[328,493,494],{},"warn",[328,496,367],{},") and optional options like ",[328,499,500],{},"minScore",". An ",[328,503,367],{}," assertion that fails causes a non-zero exit, which fails the build.",[417,506,510],{"className":507,"code":508,"language":509,"meta":422,"style":422},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F lighthouserc.js — accessibility budget configuration\nmodule.exports = {\n  ci: {\n    collect: {\n      \u002F\u002F Boot the app, then tear it down when collection finishes\n      startServerCommand: 'npm run start',\n      startServerReadyPattern: 'ready on|listening on|started server',\n      url: ['http:\u002F\u002Flocalhost:3000\u002F', 'http:\u002F\u002Flocalhost:3000\u002Fdashboard'],\n      numberOfRuns: 3, \u002F\u002F median across runs damps flaky timing\u002Fcontrast results\n    },\n    assert: {\n      assertions: {\n        \u002F\u002F CATEGORY GATE: fail if the weighted accessibility score drops below 0.95\n        'categories:accessibility': ['error', { minScore: 0.95 }],\n\n        \u002F\u002F PER-AUDIT GATES: promote high-value audits to hard failures.\n        \u002F\u002F These fail the build even if the overall score stays above 0.95.\n        'color-contrast': 'error',\n        'button-name': 'error',\n        'link-name': 'error',\n        'image-alt': 'error',\n        'label': 'error',\n        'html-has-lang': 'error',\n        'document-title': 'error',\n\n        \u002F\u002F Structural audits as warnings first; promote to error once clean\n        'heading-order': 'warn',\n        'list': 'warn',\n      },\n    },\n    upload: {\n      \u002F\u002F Attach a public report URL to each run for reviewer triage\n      target: 'temporary-public-storage',\n    },\n  },\n};\n","js",[328,511,512,517,535,540,546,552,564,575,592,606,612,618,624,630,651,658,664,670,683,695,707,719,731,743,755,760,766,779,791,797,802,808,814,825,830,836],{"__ignoreMap":422},[426,513,514],{"class":428,"line":429},[426,515,516],{"class":450},"\u002F\u002F lighthouserc.js — accessibility budget configuration\n",[426,518,519,522,525,528,532],{"class":428,"line":447},[426,520,521],{"class":440},"module",[426,523,344],{"class":524},"sVt8B",[426,526,527],{"class":440},"exports",[426,529,531],{"class":530},"szBVR"," =",[426,533,534],{"class":524}," {\n",[426,536,537],{"class":428,"line":454},[426,538,539],{"class":524},"  ci: {\n",[426,541,543],{"class":428,"line":542},4,[426,544,545],{"class":524},"    collect: {\n",[426,547,549],{"class":428,"line":548},5,[426,550,551],{"class":450},"      \u002F\u002F Boot the app, then tear it down when collection finishes\n",[426,553,555,558,561],{"class":428,"line":554},6,[426,556,557],{"class":524},"      startServerCommand: ",[426,559,560],{"class":436},"'npm run start'",[426,562,563],{"class":524},",\n",[426,565,567,570,573],{"class":428,"line":566},7,[426,568,569],{"class":524},"      startServerReadyPattern: ",[426,571,572],{"class":436},"'ready on|listening on|started server'",[426,574,563],{"class":524},[426,576,578,581,584,586,589],{"class":428,"line":577},8,[426,579,580],{"class":524},"      url: [",[426,582,583],{"class":436},"'http:\u002F\u002Flocalhost:3000\u002F'",[426,585,378],{"class":524},[426,587,588],{"class":436},"'http:\u002F\u002Flocalhost:3000\u002Fdashboard'",[426,590,591],{"class":524},"],\n",[426,593,595,598,601,603],{"class":428,"line":594},9,[426,596,597],{"class":524},"      numberOfRuns: ",[426,599,600],{"class":440},"3",[426,602,378],{"class":524},[426,604,605],{"class":450},"\u002F\u002F median across runs damps flaky timing\u002Fcontrast results\n",[426,607,609],{"class":428,"line":608},10,[426,610,611],{"class":524},"    },\n",[426,613,615],{"class":428,"line":614},11,[426,616,617],{"class":524},"    assert: {\n",[426,619,621],{"class":428,"line":620},12,[426,622,623],{"class":524},"      assertions: {\n",[426,625,627],{"class":428,"line":626},13,[426,628,629],{"class":450},"        \u002F\u002F CATEGORY GATE: fail if the weighted accessibility score drops below 0.95\n",[426,631,633,636,639,642,645,648],{"class":428,"line":632},14,[426,634,635],{"class":436},"        'categories:accessibility'",[426,637,638],{"class":524},": [",[426,640,641],{"class":436},"'error'",[426,643,644],{"class":524},", { minScore: ",[426,646,647],{"class":440},"0.95",[426,649,650],{"class":524}," }],\n",[426,652,654],{"class":428,"line":653},15,[426,655,657],{"emptyLinePlaceholder":656},true,"\n",[426,659,661],{"class":428,"line":660},16,[426,662,663],{"class":450},"        \u002F\u002F PER-AUDIT GATES: promote high-value audits to hard failures.\n",[426,665,667],{"class":428,"line":666},17,[426,668,669],{"class":450},"        \u002F\u002F These fail the build even if the overall score stays above 0.95.\n",[426,671,673,676,679,681],{"class":428,"line":672},18,[426,674,675],{"class":436},"        'color-contrast'",[426,677,678],{"class":524},": ",[426,680,641],{"class":436},[426,682,563],{"class":524},[426,684,686,689,691,693],{"class":428,"line":685},19,[426,687,688],{"class":436},"        'button-name'",[426,690,678],{"class":524},[426,692,641],{"class":436},[426,694,563],{"class":524},[426,696,698,701,703,705],{"class":428,"line":697},20,[426,699,700],{"class":436},"        'link-name'",[426,702,678],{"class":524},[426,704,641],{"class":436},[426,706,563],{"class":524},[426,708,710,713,715,717],{"class":428,"line":709},21,[426,711,712],{"class":436},"        'image-alt'",[426,714,678],{"class":524},[426,716,641],{"class":436},[426,718,563],{"class":524},[426,720,722,725,727,729],{"class":428,"line":721},22,[426,723,724],{"class":436},"        'label'",[426,726,678],{"class":524},[426,728,641],{"class":436},[426,730,563],{"class":524},[426,732,734,737,739,741],{"class":428,"line":733},23,[426,735,736],{"class":436},"        'html-has-lang'",[426,738,678],{"class":524},[426,740,641],{"class":436},[426,742,563],{"class":524},[426,744,746,749,751,753],{"class":428,"line":745},24,[426,747,748],{"class":436},"        'document-title'",[426,750,678],{"class":524},[426,752,641],{"class":436},[426,754,563],{"class":524},[426,756,758],{"class":428,"line":757},25,[426,759,657],{"emptyLinePlaceholder":656},[426,761,763],{"class":428,"line":762},26,[426,764,765],{"class":450},"        \u002F\u002F Structural audits as warnings first; promote to error once clean\n",[426,767,769,772,774,777],{"class":428,"line":768},27,[426,770,771],{"class":436},"        'heading-order'",[426,773,678],{"class":524},[426,775,776],{"class":436},"'warn'",[426,778,563],{"class":524},[426,780,782,785,787,789],{"class":428,"line":781},28,[426,783,784],{"class":436},"        'list'",[426,786,678],{"class":524},[426,788,776],{"class":436},[426,790,563],{"class":524},[426,792,794],{"class":428,"line":793},29,[426,795,796],{"class":524},"      },\n",[426,798,800],{"class":428,"line":799},30,[426,801,611],{"class":524},[426,803,805],{"class":428,"line":804},31,[426,806,807],{"class":524},"    upload: {\n",[426,809,811],{"class":428,"line":810},32,[426,812,813],{"class":450},"      \u002F\u002F Attach a public report URL to each run for reviewer triage\n",[426,815,817,820,823],{"class":428,"line":816},33,[426,818,819],{"class":524},"      target: ",[426,821,822],{"class":436},"'temporary-public-storage'",[426,824,563],{"class":524},[426,826,828],{"class":428,"line":827},34,[426,829,611],{"class":524},[426,831,833],{"class":428,"line":832},35,[426,834,835],{"class":524},"  },\n",[426,837,839],{"class":428,"line":838},36,[426,840,841],{"class":524},"};\n",[324,843,844,845,847,848,850,851,854,855,857,858,861,862,344],{},"The two assertion styles work together. ",[328,846,487],{}," with ",[328,849,500],{}," is your ",[348,852,853],{},"budget","—a floor on the aggregate weighted score. The per-audit ",[328,856,367],{}," entries are ",[348,859,860],{},"non-negotiables","—specific defects you refuse to ship even if the overall number stays healthy. Pairing them means a regression can't sneak through by failing a heavy audit while other audits happen to improve and keep the average up. For why the aggregate alone is insufficient, see ",[341,863,865],{"href":864},"\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores\u002F","interpreting Lighthouse accessibility scores",[867,868,869],"blockquote",{},[324,870,871,874,875,877,878,880],{},[348,872,873],{},"Testing Hook:"," Set ",[328,876,500],{}," to your ",[468,879,470],{}," score, not a future goal. Asserting a floor you already clear means the gate catches regressions immediately, and you raise the floor deliberately as debt is paid down.",[406,882],{},[409,884,886],{"id":885},"a-json-alternative","A JSON Alternative",[324,888,889,890,893,894,897],{},"If your team prefers JSON over a JS module, ",[328,891,892],{},"lighthouserc.json"," is equivalent and ",[328,895,896],{},"lhci"," picks it up automatically:",[417,899,903],{"className":900,"code":901,"language":902,"meta":422,"style":422},"language-json shiki shiki-themes github-light github-dark","{\n  \"ci\": {\n    \"collect\": {\n      \"startServerCommand\": \"npm run start\",\n      \"url\": [\"http:\u002F\u002Flocalhost:3000\u002F\"],\n      \"numberOfRuns\": 3\n    },\n    \"assert\": {\n      \"assertions\": {\n        \"categories:accessibility\": [\"error\", { \"minScore\": 0.95 }],\n        \"color-contrast\": \"error\",\n        \"button-name\": \"error\",\n        \"image-alt\": \"error\"\n      }\n    },\n    \"upload\": { \"target\": \"temporary-public-storage\" }\n  }\n}\n","json",[328,904,905,910,918,925,937,949,959,963,970,977,999,1010,1021,1031,1036,1040,1059,1064],{"__ignoreMap":422},[426,906,907],{"class":428,"line":429},[426,908,909],{"class":524},"{\n",[426,911,912,915],{"class":428,"line":447},[426,913,914],{"class":440},"  \"ci\"",[426,916,917],{"class":524},": {\n",[426,919,920,923],{"class":428,"line":454},[426,921,922],{"class":440},"    \"collect\"",[426,924,917],{"class":524},[426,926,927,930,932,935],{"class":428,"line":542},[426,928,929],{"class":440},"      \"startServerCommand\"",[426,931,678],{"class":524},[426,933,934],{"class":436},"\"npm run start\"",[426,936,563],{"class":524},[426,938,939,942,944,947],{"class":428,"line":548},[426,940,941],{"class":440},"      \"url\"",[426,943,638],{"class":524},[426,945,946],{"class":436},"\"http:\u002F\u002Flocalhost:3000\u002F\"",[426,948,591],{"class":524},[426,950,951,954,956],{"class":428,"line":554},[426,952,953],{"class":440},"      \"numberOfRuns\"",[426,955,678],{"class":524},[426,957,958],{"class":440},"3\n",[426,960,961],{"class":428,"line":566},[426,962,611],{"class":524},[426,964,965,968],{"class":428,"line":577},[426,966,967],{"class":440},"    \"assert\"",[426,969,917],{"class":524},[426,971,972,975],{"class":428,"line":594},[426,973,974],{"class":440},"      \"assertions\"",[426,976,917],{"class":524},[426,978,979,982,984,987,990,993,995,997],{"class":428,"line":608},[426,980,981],{"class":440},"        \"categories:accessibility\"",[426,983,638],{"class":524},[426,985,986],{"class":436},"\"error\"",[426,988,989],{"class":524},", { ",[426,991,992],{"class":440},"\"minScore\"",[426,994,678],{"class":524},[426,996,647],{"class":440},[426,998,650],{"class":524},[426,1000,1001,1004,1006,1008],{"class":428,"line":614},[426,1002,1003],{"class":440},"        \"color-contrast\"",[426,1005,678],{"class":524},[426,1007,986],{"class":436},[426,1009,563],{"class":524},[426,1011,1012,1015,1017,1019],{"class":428,"line":620},[426,1013,1014],{"class":440},"        \"button-name\"",[426,1016,678],{"class":524},[426,1018,986],{"class":436},[426,1020,563],{"class":524},[426,1022,1023,1026,1028],{"class":428,"line":626},[426,1024,1025],{"class":440},"        \"image-alt\"",[426,1027,678],{"class":524},[426,1029,1030],{"class":436},"\"error\"\n",[426,1032,1033],{"class":428,"line":632},[426,1034,1035],{"class":524},"      }\n",[426,1037,1038],{"class":428,"line":653},[426,1039,611],{"class":524},[426,1041,1042,1045,1048,1051,1053,1056],{"class":428,"line":660},[426,1043,1044],{"class":440},"    \"upload\"",[426,1046,1047],{"class":524},": { ",[426,1049,1050],{"class":440},"\"target\"",[426,1052,678],{"class":524},[426,1054,1055],{"class":436},"\"temporary-public-storage\"",[426,1057,1058],{"class":524}," }\n",[426,1060,1061],{"class":428,"line":666},[426,1062,1063],{"class":524},"  }\n",[426,1065,1066],{"class":428,"line":672},[426,1067,1068],{"class":524},"}\n",[867,1070,1071],{},[324,1072,1073,1075,1076,1079],{},[348,1074,873],{}," Use the JS config (",[328,1077,1078],{},"lighthouserc.js",") when you need comments, environment-conditional thresholds, or computed URL lists; use JSON for static, lint-friendly configs that other tooling reads.",[406,1081],{},[409,1083,1085],{"id":1084},"running-lhci-autorun-in-github-actions","Running lhci autorun in GitHub Actions",[324,1087,1088,1090,1091,1093,1094,1097],{},[328,1089,338],{}," chains collect → assert → upload in one command, reading your ",[328,1092,334],{},". The ",[328,1095,1096],{},"treosh\u002Flighthouse-ci-action"," wraps it for GitHub Actions, surfaces the assertion results, and can attach artifacts to the run.",[417,1099,1103],{"className":1100,"code":1101,"language":1102,"meta":422,"style":422},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Flighthouse-budget.yml\nname: Lighthouse Accessibility Budget\non:\n  pull_request:\n    branches: [main]\njobs:\n  a11y-budget:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 20\n          cache: npm\n\n      - run: npm ci\n      - run: npm run build   # produce the production build lhci will serve\n\n      - name: Run Lighthouse CI and assert the accessibility budget\n        uses: treosh\u002Flighthouse-ci-action@v12\n        with:\n          configPath: .\u002Flighthouserc.js\n          # The action exits non-zero when an `error` assertion fails,\n          # which fails the job and blocks the PR.\n          uploadArtifacts: true        # attach HTML reports to the workflow run\n          temporaryPublicStorage: true # produce a shareable report URL per run\n","yaml",[328,1104,1105,1110,1121,1129,1136,1149,1156,1163,1173,1180,1193,1197,1208,1215,1225,1235,1239,1251,1265,1269,1280,1290,1296,1306,1311,1316,1329],{"__ignoreMap":422},[426,1106,1107],{"class":428,"line":429},[426,1108,1109],{"class":450},"# .github\u002Fworkflows\u002Flighthouse-budget.yml\n",[426,1111,1112,1116,1118],{"class":428,"line":447},[426,1113,1115],{"class":1114},"s9eBZ","name",[426,1117,678],{"class":524},[426,1119,1120],{"class":436},"Lighthouse Accessibility Budget\n",[426,1122,1123,1126],{"class":428,"line":454},[426,1124,1125],{"class":440},"on",[426,1127,1128],{"class":524},":\n",[426,1130,1131,1134],{"class":428,"line":542},[426,1132,1133],{"class":1114},"  pull_request",[426,1135,1128],{"class":524},[426,1137,1138,1141,1143,1146],{"class":428,"line":548},[426,1139,1140],{"class":1114},"    branches",[426,1142,638],{"class":524},[426,1144,1145],{"class":436},"main",[426,1147,1148],{"class":524},"]\n",[426,1150,1151,1154],{"class":428,"line":554},[426,1152,1153],{"class":1114},"jobs",[426,1155,1128],{"class":524},[426,1157,1158,1161],{"class":428,"line":566},[426,1159,1160],{"class":1114},"  a11y-budget",[426,1162,1128],{"class":524},[426,1164,1165,1168,1170],{"class":428,"line":577},[426,1166,1167],{"class":1114},"    runs-on",[426,1169,678],{"class":524},[426,1171,1172],{"class":436},"ubuntu-latest\n",[426,1174,1175,1178],{"class":428,"line":594},[426,1176,1177],{"class":1114},"    steps",[426,1179,1128],{"class":524},[426,1181,1182,1185,1188,1190],{"class":428,"line":608},[426,1183,1184],{"class":524},"      - ",[426,1186,1187],{"class":1114},"uses",[426,1189,678],{"class":524},[426,1191,1192],{"class":436},"actions\u002Fcheckout@v4\n",[426,1194,1195],{"class":428,"line":614},[426,1196,657],{"emptyLinePlaceholder":656},[426,1198,1199,1201,1203,1205],{"class":428,"line":620},[426,1200,1184],{"class":524},[426,1202,1187],{"class":1114},[426,1204,678],{"class":524},[426,1206,1207],{"class":436},"actions\u002Fsetup-node@v4\n",[426,1209,1210,1213],{"class":428,"line":626},[426,1211,1212],{"class":1114},"        with",[426,1214,1128],{"class":524},[426,1216,1217,1220,1222],{"class":428,"line":632},[426,1218,1219],{"class":1114},"          node-version",[426,1221,678],{"class":524},[426,1223,1224],{"class":440},"20\n",[426,1226,1227,1230,1232],{"class":428,"line":653},[426,1228,1229],{"class":1114},"          cache",[426,1231,678],{"class":524},[426,1233,1234],{"class":436},"npm\n",[426,1236,1237],{"class":428,"line":660},[426,1238,657],{"emptyLinePlaceholder":656},[426,1240,1241,1243,1246,1248],{"class":428,"line":666},[426,1242,1184],{"class":524},[426,1244,1245],{"class":1114},"run",[426,1247,678],{"class":524},[426,1249,1250],{"class":436},"npm ci\n",[426,1252,1253,1255,1257,1259,1262],{"class":428,"line":672},[426,1254,1184],{"class":524},[426,1256,1245],{"class":1114},[426,1258,678],{"class":524},[426,1260,1261],{"class":436},"npm run build",[426,1263,1264],{"class":450},"   # produce the production build lhci will serve\n",[426,1266,1267],{"class":428,"line":685},[426,1268,657],{"emptyLinePlaceholder":656},[426,1270,1271,1273,1275,1277],{"class":428,"line":697},[426,1272,1184],{"class":524},[426,1274,1115],{"class":1114},[426,1276,678],{"class":524},[426,1278,1279],{"class":436},"Run Lighthouse CI and assert the accessibility budget\n",[426,1281,1282,1285,1287],{"class":428,"line":709},[426,1283,1284],{"class":1114},"        uses",[426,1286,678],{"class":524},[426,1288,1289],{"class":436},"treosh\u002Flighthouse-ci-action@v12\n",[426,1291,1292,1294],{"class":428,"line":721},[426,1293,1212],{"class":1114},[426,1295,1128],{"class":524},[426,1297,1298,1301,1303],{"class":428,"line":733},[426,1299,1300],{"class":1114},"          configPath",[426,1302,678],{"class":524},[426,1304,1305],{"class":436},".\u002Flighthouserc.js\n",[426,1307,1308],{"class":428,"line":745},[426,1309,1310],{"class":450},"          # The action exits non-zero when an `error` assertion fails,\n",[426,1312,1313],{"class":428,"line":757},[426,1314,1315],{"class":450},"          # which fails the job and blocks the PR.\n",[426,1317,1318,1321,1323,1326],{"class":428,"line":762},[426,1319,1320],{"class":1114},"          uploadArtifacts",[426,1322,678],{"class":524},[426,1324,1325],{"class":440},"true",[426,1327,1328],{"class":450},"        # attach HTML reports to the workflow run\n",[426,1330,1331,1334,1336,1338],{"class":428,"line":768},[426,1332,1333],{"class":1114},"          temporaryPublicStorage",[426,1335,678],{"class":524},[426,1337,1325],{"class":440},[426,1339,1340],{"class":450}," # produce a shareable report URL per run\n",[324,1342,1343,1344,1346,1347,1349,1350,344],{},"Because the action propagates ",[328,1345,896],{},"'s exit code, a failed ",[328,1348,367],{}," assertion fails the job automatically—no extra scripting required. To make the gate mandatory, mark this job as a required status check in your branch protection rules, the same way you would for any other ",[341,1351,1353],{"href":1352},"\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002F","CI\u002FCD accessibility gate",[867,1355,1356],{},[324,1357,1358,1360],{},[348,1359,873],{}," Confirm the job is listed under branch protection's required checks. An assertion that fails the job but isn't required will let a regressing PR merge anyway.",[406,1362],{},[409,1364,1366],{"id":1365},"failing-the-build-on-regression","Failing the Build on Regression",[324,1368,1369],{},"The gate is only as good as its determinism. Two settings keep it from flaking and either blocking clean PRs or waving through real regressions:",[352,1371,1372,1381],{},[355,1373,1374,1380],{},[348,1375,1376,1379],{},[328,1377,1378],{},"numberOfRuns: 3"," (or more)"," with the median makes timing- and layout-sensitive audits stable. Accessibility audits are largely deterministic, but emulated viewports and hover-dependent contrast can wobble on a single run.",[355,1382,1383,1386,1387,1390],{},[348,1384,1385],{},"Audit the production build",", served by ",[328,1388,1389],{},"startServerCommand",", not the dev server. Dev builds inject extra markup and warning UI that can change audit results.",[324,1392,1393,1394,1396,1397,1399],{},"When an ",[328,1395,367],{}," assertion fails, the action's log prints the exact assertion, the expected threshold, and the actual value—so a reviewer sees \"",[328,1398,363],{}," failed: found 3 nodes\" rather than a bare red X. Pair that with the uploaded report URL and the regression is diagnosable from the PR alone.",[417,1401,1403],{"className":419,"code":1402,"language":421,"meta":422,"style":422},"# Reproduce the CI gate locally before pushing\nnpm run build\nnpx lhci autorun --config=.\u002Flighthouserc.js\necho \"Exit code: $?\"   # non-zero means an error assertion failed\n",[328,1404,1405,1410,1420,1432],{"__ignoreMap":422},[426,1406,1407],{"class":428,"line":429},[426,1408,1409],{"class":450},"# Reproduce the CI gate locally before pushing\n",[426,1411,1412,1414,1417],{"class":428,"line":447},[426,1413,433],{"class":432},[426,1415,1416],{"class":436}," run",[426,1418,1419],{"class":436}," build\n",[426,1421,1422,1424,1426,1429],{"class":428,"line":454},[426,1423,457],{"class":432},[426,1425,460],{"class":436},[426,1427,1428],{"class":436}," autorun",[426,1430,1431],{"class":440}," --config=.\u002Flighthouserc.js\n",[426,1433,1434,1437,1440,1443,1446],{"class":428,"line":542},[426,1435,1436],{"class":440},"echo",[426,1438,1439],{"class":436}," \"Exit code: ",[426,1441,1442],{"class":440},"$?",[426,1444,1445],{"class":436},"\"",[426,1447,1448],{"class":450},"   # non-zero means an error assertion failed\n",[867,1450,1451],{},[324,1452,1453,1455,1456,1459],{},[348,1454,873],{}," Run ",[328,1457,1458],{},"npx lhci autorun"," locally and read the exit code. A non-zero exit is precisely what fails CI; reproducing it locally turns a red pipeline into a fast feedback loop.",[406,1461],{},[409,1463,1465],{"id":1464},"per-audit-allowlist-for-known-debt","Per-Audit Allowlist for Known Debt",[324,1467,1468,1469,1472],{},"Real codebases carry accessibility debt you can't fix in the same PR that adds the gate. The wrong move is to lower the whole budget or disable the gate; the right move is a ",[468,1470,1471],{},"scoped"," exception that keeps every other check strict.",[324,1474,1475,1476,1478,1479,1481,1482,344],{},"Use ",[328,1477,491],{}," (or ",[328,1480,494],{},") on the specific audit you're temporarily tolerating, ideally with a comment and a tracking ticket. This is an allowlist, not a surrender—every other audit stays at ",[328,1483,367],{},[417,1485,1487],{"className":507,"code":1486,"language":509,"meta":422,"style":422},"\u002F\u002F lighthouserc.js — scoped allowlist for tracked debt\nmodule.exports = {\n  ci: {\n    collect: { url: ['http:\u002F\u002Flocalhost:3000\u002F'], numberOfRuns: 3 },\n    assert: {\n      \u002F\u002F Inherit a strict preset, then override only the debt items\n      preset: 'lighthouse:recommended',\n      assertions: {\n        \u002F\u002F Keep the accessibility budget strict\n        'categories:accessibility': ['error', { minScore: 0.95 }],\n        'color-contrast': 'error',\n        'button-name': 'error',\n\n        \u002F\u002F KNOWN DEBT — downgrade, don't delete. Re-promote when fixed.\n        \u002F\u002F JIRA A11Y-482: legacy date-picker fires aria-required-children.\n        'aria-required-children': 'warn',\n\n        \u002F\u002F ALLOWED FAILURE — third-party embed we don't control yet.\n        \u002F\u002F JIRA A11Y-510: vendor widget lacks landmark; remove when vendor ships fix.\n        'landmark-one-main': 'off',\n      },\n    },\n  },\n};\n",[328,1488,1489,1494,1506,1510,1525,1529,1534,1544,1548,1553,1567,1577,1587,1591,1596,1601,1612,1616,1621,1626,1638,1642,1646,1650],{"__ignoreMap":422},[426,1490,1491],{"class":428,"line":429},[426,1492,1493],{"class":450},"\u002F\u002F lighthouserc.js — scoped allowlist for tracked debt\n",[426,1495,1496,1498,1500,1502,1504],{"class":428,"line":447},[426,1497,521],{"class":440},[426,1499,344],{"class":524},[426,1501,527],{"class":440},[426,1503,531],{"class":530},[426,1505,534],{"class":524},[426,1507,1508],{"class":428,"line":454},[426,1509,539],{"class":524},[426,1511,1512,1515,1517,1520,1522],{"class":428,"line":542},[426,1513,1514],{"class":524},"    collect: { url: [",[426,1516,583],{"class":436},[426,1518,1519],{"class":524},"], numberOfRuns: ",[426,1521,600],{"class":440},[426,1523,1524],{"class":524}," },\n",[426,1526,1527],{"class":428,"line":548},[426,1528,617],{"class":524},[426,1530,1531],{"class":428,"line":554},[426,1532,1533],{"class":450},"      \u002F\u002F Inherit a strict preset, then override only the debt items\n",[426,1535,1536,1539,1542],{"class":428,"line":566},[426,1537,1538],{"class":524},"      preset: ",[426,1540,1541],{"class":436},"'lighthouse:recommended'",[426,1543,563],{"class":524},[426,1545,1546],{"class":428,"line":577},[426,1547,623],{"class":524},[426,1549,1550],{"class":428,"line":594},[426,1551,1552],{"class":450},"        \u002F\u002F Keep the accessibility budget strict\n",[426,1554,1555,1557,1559,1561,1563,1565],{"class":428,"line":608},[426,1556,635],{"class":436},[426,1558,638],{"class":524},[426,1560,641],{"class":436},[426,1562,644],{"class":524},[426,1564,647],{"class":440},[426,1566,650],{"class":524},[426,1568,1569,1571,1573,1575],{"class":428,"line":614},[426,1570,675],{"class":436},[426,1572,678],{"class":524},[426,1574,641],{"class":436},[426,1576,563],{"class":524},[426,1578,1579,1581,1583,1585],{"class":428,"line":620},[426,1580,688],{"class":436},[426,1582,678],{"class":524},[426,1584,641],{"class":436},[426,1586,563],{"class":524},[426,1588,1589],{"class":428,"line":626},[426,1590,657],{"emptyLinePlaceholder":656},[426,1592,1593],{"class":428,"line":632},[426,1594,1595],{"class":450},"        \u002F\u002F KNOWN DEBT — downgrade, don't delete. Re-promote when fixed.\n",[426,1597,1598],{"class":428,"line":653},[426,1599,1600],{"class":450},"        \u002F\u002F JIRA A11Y-482: legacy date-picker fires aria-required-children.\n",[426,1602,1603,1606,1608,1610],{"class":428,"line":660},[426,1604,1605],{"class":436},"        'aria-required-children'",[426,1607,678],{"class":524},[426,1609,776],{"class":436},[426,1611,563],{"class":524},[426,1613,1614],{"class":428,"line":666},[426,1615,657],{"emptyLinePlaceholder":656},[426,1617,1618],{"class":428,"line":672},[426,1619,1620],{"class":450},"        \u002F\u002F ALLOWED FAILURE — third-party embed we don't control yet.\n",[426,1622,1623],{"class":428,"line":685},[426,1624,1625],{"class":450},"        \u002F\u002F JIRA A11Y-510: vendor widget lacks landmark; remove when vendor ships fix.\n",[426,1627,1628,1631,1633,1636],{"class":428,"line":697},[426,1629,1630],{"class":436},"        'landmark-one-main'",[426,1632,678],{"class":524},[426,1634,1635],{"class":436},"'off'",[426,1637,563],{"class":524},[426,1639,1640],{"class":428,"line":709},[426,1641,796],{"class":524},[426,1643,1644],{"class":428,"line":721},[426,1645,611],{"class":524},[426,1647,1648],{"class":428,"line":733},[426,1649,835],{"class":524},[426,1651,1652],{"class":428,"line":745},[426,1653,841],{"class":524},[324,1655,1656,1657,1660,1661,86,1663,1666],{},"For audits where ",[468,1658,1659],{},"some"," failing nodes are tolerated but the count must not grow, use ",[328,1662,500],{},[328,1664,1665],{},"maxLength","-style options on the specific assertion rather than turning it off entirely, so new instances of the same defect still fail the build.",[867,1668,1669],{},[324,1670,1671,1673,1674,1676],{},[348,1672,873],{}," Tie every allowlist entry to a ticket ID in a comment. Audit the config in code review: an ",[328,1675,491],{}," without a tracking reference is invisible debt that will quietly become permanent.",[406,1678],{},[409,1680,1682],{"id":1681},"how-to-verify","How to Verify",[324,1684,1685],{},"Verify the gate works with both a tool check and a manual deliberate-regression test:",[324,1687,1688,1691],{},[348,1689,1690],{},"Tool check."," Run the gate locally against your current build and confirm it passes, then read the assertion results to confirm the thresholds are what you intend:",[417,1693,1695],{"className":419,"code":1694,"language":421,"meta":422,"style":422},"npm run build && npx lhci autorun --config=.\u002Flighthouserc.js\n# Exit 0 = all assertions met. Inspect the uploaded report URL printed in the log.\n",[328,1696,1697,1717],{"__ignoreMap":422},[426,1698,1699,1701,1703,1706,1709,1711,1713,1715],{"class":428,"line":429},[426,1700,433],{"class":432},[426,1702,1416],{"class":436},[426,1704,1705],{"class":436}," build",[426,1707,1708],{"class":524}," && ",[426,1710,457],{"class":432},[426,1712,460],{"class":436},[426,1714,1428],{"class":436},[426,1716,1431],{"class":440},[426,1718,1719],{"class":428,"line":447},[426,1720,1721],{"class":450},"# Exit 0 = all assertions met. Inspect the uploaded report URL printed in the log.\n",[324,1723,1724,1727,1728,1731,1732,1734,1735,1737],{},[348,1725,1726],{},"Manual regression test."," Prove the gate actually blocks a regression—a gate you've never seen fail is a gate you can't trust. Introduce a known violation on a branch (drop the ",[328,1729,1730],{},"alt"," on an image, or set body text to a contrast-failing color), push the PR, and confirm the GitHub Actions job turns red with the ",[328,1733,390],{}," or ",[328,1736,363],{}," assertion named in the log. Revert once you've seen it fail. This single test validates the entire chain: collect, assert, exit code, and branch protection.",[324,1739,1740,1741,344],{},"For an additional manual pass on the things this score can't see (keyboard, focus order, screen-reader meaning), pair the gate with the manual checklist in ",[341,1742,865],{"href":864},[406,1744],{},[409,1746,1748],{"id":1747},"common-a11y-mistakes","Common a11y Mistakes",[1750,1751,1752,1766,1775,1783,1792,1803],"ol",{},[355,1753,1754,1762,1763,1765],{},[348,1755,1756,1757,1759,1760,344],{},"Asserting only ",[328,1758,487],{}," ",[328,1761,500],{}," The aggregate can hide a heavy audit failing while others improve. Add per-audit ",[328,1764,367],{}," assertions for non-negotiables.",[355,1767,1768,1774],{},[348,1769,1770,1771,1773],{},"Setting an aspirational ",[328,1772,500],{}," you don't yet meet."," The build is red from day one and the team disables the gate. Floor at the current score, then ratchet up.",[355,1776,1777,1780,1781,344],{},[348,1778,1779],{},"Auditing the dev server."," Dev-only markup and warnings skew results. Always serve the production build via ",[328,1782,1389],{},[355,1784,1785,1788,1789,1791],{},[348,1786,1787],{},"Single-run audits."," Timing-sensitive checks flake. Use ",[328,1790,1378],{}," with the median.",[355,1793,1794,1797,1798,86,1800,1802],{},[348,1795,1796],{},"Disabling the whole gate for one piece of debt."," Use a scoped ",[328,1799,491],{},[328,1801,494],{}," on the specific audit with a tracking ticket, never a global downgrade.",[355,1804,1805,1808],{},[348,1806,1807],{},"Forgetting branch protection."," A failing job that isn't a required check lets the PR merge anyway.",[406,1810],{},[409,1812,1814],{"id":1813},"conclusion","Conclusion",[324,1816,1817,1818,1759,1820,1822,1823,1825,1826,1828],{},"A Lighthouse CI accessibility budget converts a one-time audit into a standing guarantee: scores can't quietly regress because a drop below your floor or a failure of a non-negotiable audit fails the pull request. Pair a ",[328,1819,487],{},[328,1821,500],{}," with per-audit ",[328,1824,367],{}," assertions, run it through ",[328,1827,338],{}," in GitHub Actions as a required check, median multiple runs against the production build, and scope known debt with tracked allowlist entries. Then prove it by watching a deliberate regression turn the build red.",[406,1830],{},[409,1832,1834],{"id":1833},"frequently-asked-questions","Frequently Asked Questions",[324,1836,1837,1840,1841,1843,1844,1846,1847,1849,1850,1852,1853,1855,1856,378,1858,1860],{},[348,1838,1839],{},"How do I make Lighthouse CI fail my build when the accessibility score drops?","\nAdd a ",[328,1842,487],{}," assertion with severity ",[328,1845,367],{}," and a ",[328,1848,500],{}," floor in your ",[328,1851,334],{}," config, then run it via ",[328,1854,338],{},". When the measured score falls below ",[328,1857,500],{},[328,1859,896],{}," exits non-zero, which fails the CI job. With the GitHub Actions job marked as a required status check, that failure blocks the pull request from merging.",[324,1862,1863,1866,1867,1759,1869,1871,1872,378,1874,378,1876,378,1878,1881,1882,1884],{},[348,1864,1865],{},"Should I assert the category score or individual audits?","\nBoth. The ",[328,1868,487],{},[328,1870,500],{}," is your aggregate budget, but it can mask a specific heavy audit failing if other audits improve. Promote your non-negotiable audits—",[328,1873,363],{},[328,1875,377],{},[328,1877,390],{},[328,1879,1880],{},"label","—to ",[328,1883,367],{}," assertions so those defects fail the build regardless of where the overall score lands.",[324,1886,1887,1890,1891,1734,1893,1895,1896,1898,1899,1901,1902,1904],{},[348,1888,1889],{},"How do I handle known accessibility debt without disabling the whole gate?","\nUse a scoped allowlist: set the specific failing audit to ",[328,1892,494],{},[328,1894,491],{}," in ",[328,1897,483],{},", leave every other audit at ",[328,1900,367],{},", and add a comment with a tracking ticket. This keeps the gate strict everywhere except the one tolerated item, and the ticket reference makes the debt visible in code review so it gets re-promoted to ",[328,1903,367],{}," once fixed.",[324,1906,1907,1910,1911,1914,1915,1917],{},[348,1908,1909],{},"Why does my Lighthouse CI score fluctuate between runs?","\nSome audits are sensitive to timing, viewport emulation, or hover-dependent contrast, so a single run can vary. Set ",[328,1912,1913],{},"numberOfRuns"," to 3 or more and Lighthouse CI uses the median, which is far more stable. Also audit the production build via ",[328,1916,1389],{}," rather than the dev server, since dev-only markup can change results between runs.",[324,1919,1920,1923,1924,1926,1927,1929,1930,1933,1934,1759,1936,1938,1939,1941],{},[348,1921,1922],{},"What's the difference between Lighthouse CI assertions and a performance budget file?","\nAssertions in ",[328,1925,483],{}," cover any audit or category, including accessibility, with severities and ",[328,1928,500],{},". A ",[328,1931,1932],{},"budgets.json"," performance budget targets resource sizes and timings specifically. For accessibility gating you use assertions—",[328,1935,487],{},[328,1937,500],{}," plus per-audit ",[328,1940,367],{}," entries—not the performance budget file.",[406,1943],{},[409,1945,1947],{"id":1946},"related-guides","Related guides",[352,1949,1950,1954,1958],{},[355,1951,1952],{},[341,1953,225],{"href":343},[355,1955,1956],{},[341,1957,231],{"href":864},[355,1959,1960],{},[341,1961,297],{"href":1352},[1963,1964,1965],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":422,"searchDepth":447,"depth":447,"links":1967},[1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978],{"id":411,"depth":447,"text":412},{"id":476,"depth":447,"text":477},{"id":885,"depth":447,"text":886},{"id":1084,"depth":447,"text":1085},{"id":1365,"depth":447,"text":1366},{"id":1464,"depth":447,"text":1465},{"id":1681,"depth":447,"text":1682},{"id":1747,"depth":447,"text":1748},{"id":1813,"depth":447,"text":1814},{"id":1833,"depth":447,"text":1834},{"id":1946,"depth":447,"text":1947},null,"Stop accessibility scores from regressing—configure Lighthouse CI assertions and budgets so a drop below your accessibility threshold fails the pull request.","md",{},false,{"title":237,"description":1980},"QFkr5RQCFTVQW32d8SbF911IxcrhO-vvUrgKxZizQFo",[1987,2026,2027,2090],{"title":5,"path":6,"stem":7,"children":1988},[1989,1990,1993,1996,2002,2008,2017,2023],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":1991},[1992],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":1994},[1995],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":1997},[1998,1999],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2000},[2001],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2003},[2004,2005],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2006},[2007],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2009},[2010,2011,2014],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2012},[2013],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2015},[2016],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2018},[2019,2020],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2021},[2022],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2024},[2025],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2028},[2029,2030,2036,2048,2060,2063,2072,2084],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2031},[2032,2033],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2034},[2035],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2037},[2038,2039,2042,2045],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2040},[2041],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2043},[2044],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2046},[2047],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2049},[2050,2051,2054,2057],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2052},[2053],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2055},[2056],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2058},[2059],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2061},[2062],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2064},[2065,2066,2069],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2067},[2068],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2070},[2071],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2073},[2074,2075,2078,2081],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2076},[2077],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2079},[2080],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2082},[2083],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2085},[2086,2087],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2088},[2089],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2091},[2092,2093,2102,2111,2120,2129],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2094},[2095,2096,2099],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2097},[2098],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2100},[2101],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2103},[2104,2105,2108],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2106},[2107],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2109},[2110],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2112},[2113,2114,2117],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2115},[2116],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2118},[2119],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2121},[2122,2123,2126],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2124},[2125],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2127},[2128],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2130},[2131,2132,2135],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2133},[2134],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2136},[2137],{"title":309,"path":310,"stem":311},1781785524257]