[{"data":1,"prerenderedAt":2863},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002F":314,"content-navigation":2711},[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":297,"body":316,"date":2704,"description":2705,"extension":2706,"image":2704,"meta":2707,"modifiedAt":2704,"navigation":584,"noindex":2708,"path":298,"publishedAt":2704,"seo":2709,"stem":299,"updatedAt":2704,"__hash__":2710},"content\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Findex.md",{"type":317,"value":318,"toc":2692},"minimark",[319,323,355,360,386,391,405,408,413,423,426,513,528,537,539,543,546,1327,1334,1399,1406,1588,1599,1601,1605,1608,1743,1745,1749,1768,1923,1933,1944,1946,1950,1965,2011,2216,2226,2228,2232,2243,2318,2448,2454,2456,2460,2463,2505,2551,2562,2564,2568,2595,2597,2601,2612,2621,2629,2635,2647,2656,2658,2662,2688],[320,321,297],"h1",{"id":322},"gating-accessibility-in-cicd-pipelines",[324,325,326,327,332,333,337,338,341,342,346,347,350,351,354],"p",{},"Accessibility only stays fixed when a machine refuses to merge regressions. This guide, part of ",[328,329,331],"a",{"href":330},"\u002Ftesting-and-automating-accessibility\u002F","Testing and Automating Accessibility",", shows frontend engineers how to turn ",[334,335,336],"code",{},"axe-core",", ",[334,339,340],{},"jest-axe",", Playwright, and Lighthouse CI into a layered ",[343,344,345],"strong",{},"release gate"," inside GitHub Actions. The goal is concrete: a violation of ",[334,348,349],{},"4.1.2 Name, Role, Value"," or ",[334,352,353],{},"2.1.1 Keyboard"," returns a non-zero exit code, the status check turns red, and branch protection blocks the merge until the tree is green again. Manual audits find nuance; a CI gate enforces the floor on every pull request, automatically, forever.",[324,356,357],{},[343,358,359],{},"WCAG Coverage Mapping",[361,362,363,369,374,380],"ul",{},[364,365,366,368],"li",{},[334,367,349],{}," (Level A) — programmatic name\u002Frole checks via axe rules",[364,370,371,373],{},[334,372,353],{}," (Level A) — Playwright keyboard-flow assertions",[364,375,376,379],{},[334,377,378],{},"1.4.3 Contrast (Minimum)"," (Level AA) — color-contrast rule + Lighthouse audit",[364,381,382,385],{},[334,383,384],{},"4.1.3 Status Messages"," (Level AA) — live-region assertions in e2e tests",[324,387,388],{},[343,389,390],{},"Gate Design Principles",[361,392,393,396,399,402],{},[364,394,395],{},"Run the cheapest, fastest check first; fail fast before spending CI minutes.",[364,397,398],{},"Each stage maps to a distinct accessibility failure mode—unit, integration, and page-level.",[364,400,401],{},"A red required check is the only thing that actually blocks a merge; logs alone do not.",[364,403,404],{},"Ship the gate incrementally with a baseline so legacy debt never blocks day-one adoption.",[406,407],"hr",{},[409,410,412],"h2",{"id":411},"the-gate-strategy-which-tool-runs-where-and-why","The Gate Strategy: Which Tool Runs Where, and Why",[324,414,415,416,418,419,422],{},"No single tool catches every accessibility defect, so the gate is a pipeline of complementary stages ordered by speed and blast radius. ",[334,417,340],{}," runs against rendered component markup in jsdom—it is fast, deterministic, and catches missing labels, invalid ARIA, and broken name\u002Frole\u002Fvalue contracts at the unit level. Playwright with ",[334,420,421],{},"@axe-core\u002Fplaywright"," runs against the fully hydrated, routed application in a real browser, catching defects that only appear after client-side rendering, portals, and focus management resolve. Lighthouse CI scores rendered pages and enforces a page-level accessibility budget, catching contrast and document-structure issues across whole routes.",[324,424,425],{},"The ordering matters. Unit tests run in seconds and fail before you spin up a browser, so a broken button label never wastes a Playwright run. Each layer is scoped to the failure mode it detects best, which keeps signal high and false positives low.",[427,428,429,448],"table",{},[430,431,432],"thead",{},[433,434,435,439,442,445],"tr",{},[436,437,438],"th",{},"Stage",[436,440,441],{},"Tool",[436,443,444],{},"Scope",[436,446,447],{},"Primary WCAG signal",[449,450,451,472,496],"tbody",{},[433,452,453,457,461,464],{},[454,455,456],"td",{},"Unit",[454,458,459],{},[334,460,340],{},[454,462,463],{},"Single component in jsdom",[454,465,466,337,469],{},[334,467,468],{},"4.1.2",[334,470,471],{},"1.3.1",[433,473,474,477,482,485],{},[454,475,476],{},"E2E",[454,478,479,480],{},"Playwright + ",[334,481,421],{},[454,483,484],{},"Hydrated route, real browser",[454,486,487,337,490,337,493],{},[334,488,489],{},"2.1.1",[334,491,492],{},"4.1.3",[334,494,495],{},"2.4.3",[433,497,498,501,504,507],{},[454,499,500],{},"Page budget",[454,502,503],{},"Lighthouse CI",[454,505,506],{},"Whole-page score",[454,508,509,512],{},[334,510,511],{},"1.4.3",", document structure",[324,514,515,516,519,520,523,524,527],{},"Component-level rules are detailed in ",[328,517,261],{"href":518},"\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002F","; the browser layer is covered in ",[328,521,279],{"href":522},"\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002F","; page budgets live in ",[328,525,225],{"href":526},"\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002F",".",[529,530,531],"blockquote",{},[324,532,533,536],{},[343,534,535],{},"Gate Hook:"," Treat each stage as an independent required check. If one tool's results are noisy, you can tune or quarantine that stage without disabling the entire gate.",[406,538],{},[409,540,542],{"id":541},"a-complete-github-actions-workflow","A Complete GitHub Actions Workflow",[324,544,545],{},"The workflow below runs the three stages as separate jobs so each surfaces as its own status check on the pull request. Install runs once and the result is cached; the three test jobs fan out from it. Every job ends in a non-zero exit code on violation—that exit code is what GitHub converts into a red check.",[547,548,553],"pre",{"className":549,"code":550,"language":551,"meta":552,"style":552},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fa11y-gate.yml\nname: a11y-gate\n\non:\n  pull_request:\n    branches: [main]\n  push:\n    branches: [main]\n\n# Cancel superseded runs on the same PR to save CI minutes.\nconcurrency:\n  group: a11y-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  install:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 20\n          cache: npm            # cache the npm registry between runs\n      - run: npm ci             # exits non-zero on lockfile drift\n      - run: npm run build      # build once; e2e + lighthouse reuse it\n      - uses: actions\u002Fupload-artifact@v4\n        with:\n          name: dist\n          path: dist\u002F\n\n  jest-axe:\n    needs: install\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with: { node-version: 20, cache: npm }\n      - run: npm ci\n      # Fails the job (exit 1) the moment any axe violation is asserted.\n      - run: npm run test:a11y -- --ci\n\n  playwright-a11y:\n    needs: install\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with: { node-version: 20, cache: npm }\n      - run: npm ci\n      - run: npx playwright install --with-deps chromium\n      - uses: actions\u002Fdownload-artifact@v4\n        with: { name: dist, path: dist\u002F }\n      - run: npm run test:e2e:a11y       # non-zero on any violation\n      - uses: actions\u002Fupload-artifact@v4\n        if: always()                     # keep the report even on failure\n        with:\n          name: playwright-a11y-report\n          path: playwright-report\u002F\n\n  lighthouse-ci:\n    needs: install\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with: { node-version: 20, cache: npm }\n      - run: npm ci\n      - uses: actions\u002Fdownload-artifact@v4\n        with: { name: dist, path: dist\u002F }\n      # assertion failures in lighthouserc.js exit non-zero -> red check\n      - run: npx @lhci\u002Fcli autorun\n","yaml","",[334,554,555,564,579,586,596,604,619,627,638,643,649,657,668,679,684,692,700,711,719,733,745,753,764,778,794,809,821,828,839,850,855,863,874,883,890,901,912,940,952,958,970,975,983,992,1001,1008,1019,1030,1053,1064,1076,1088,1114,1129,1140,1154,1161,1171,1181,1186,1194,1203,1212,1219,1230,1241,1264,1275,1286,1309,1315],{"__ignoreMap":552},[556,557,560],"span",{"class":558,"line":559},"line",1,[556,561,563],{"class":562},"sJ8bj","# .github\u002Fworkflows\u002Fa11y-gate.yml\n",[556,565,567,571,575],{"class":558,"line":566},2,[556,568,570],{"class":569},"s9eBZ","name",[556,572,574],{"class":573},"sVt8B",": ",[556,576,578],{"class":577},"sZZnC","a11y-gate\n",[556,580,582],{"class":558,"line":581},3,[556,583,585],{"emptyLinePlaceholder":584},true,"\n",[556,587,589,593],{"class":558,"line":588},4,[556,590,592],{"class":591},"sj4cs","on",[556,594,595],{"class":573},":\n",[556,597,599,602],{"class":558,"line":598},5,[556,600,601],{"class":569},"  pull_request",[556,603,595],{"class":573},[556,605,607,610,613,616],{"class":558,"line":606},6,[556,608,609],{"class":569},"    branches",[556,611,612],{"class":573},": [",[556,614,615],{"class":577},"main",[556,617,618],{"class":573},"]\n",[556,620,622,625],{"class":558,"line":621},7,[556,623,624],{"class":569},"  push",[556,626,595],{"class":573},[556,628,630,632,634,636],{"class":558,"line":629},8,[556,631,609],{"class":569},[556,633,612],{"class":573},[556,635,615],{"class":577},[556,637,618],{"class":573},[556,639,641],{"class":558,"line":640},9,[556,642,585],{"emptyLinePlaceholder":584},[556,644,646],{"class":558,"line":645},10,[556,647,648],{"class":562},"# Cancel superseded runs on the same PR to save CI minutes.\n",[556,650,652,655],{"class":558,"line":651},11,[556,653,654],{"class":569},"concurrency",[556,656,595],{"class":573},[556,658,660,663,665],{"class":558,"line":659},12,[556,661,662],{"class":569},"  group",[556,664,574],{"class":573},[556,666,667],{"class":577},"a11y-${{ github.ref }}\n",[556,669,671,674,676],{"class":558,"line":670},13,[556,672,673],{"class":569},"  cancel-in-progress",[556,675,574],{"class":573},[556,677,678],{"class":591},"true\n",[556,680,682],{"class":558,"line":681},14,[556,683,585],{"emptyLinePlaceholder":584},[556,685,687,690],{"class":558,"line":686},15,[556,688,689],{"class":569},"jobs",[556,691,595],{"class":573},[556,693,695,698],{"class":558,"line":694},16,[556,696,697],{"class":569},"  install",[556,699,595],{"class":573},[556,701,703,706,708],{"class":558,"line":702},17,[556,704,705],{"class":569},"    runs-on",[556,707,574],{"class":573},[556,709,710],{"class":577},"ubuntu-latest\n",[556,712,714,717],{"class":558,"line":713},18,[556,715,716],{"class":569},"    steps",[556,718,595],{"class":573},[556,720,722,725,728,730],{"class":558,"line":721},19,[556,723,724],{"class":573},"      - ",[556,726,727],{"class":569},"uses",[556,729,574],{"class":573},[556,731,732],{"class":577},"actions\u002Fcheckout@v4\n",[556,734,736,738,740,742],{"class":558,"line":735},20,[556,737,724],{"class":573},[556,739,727],{"class":569},[556,741,574],{"class":573},[556,743,744],{"class":577},"actions\u002Fsetup-node@v4\n",[556,746,748,751],{"class":558,"line":747},21,[556,749,750],{"class":569},"        with",[556,752,595],{"class":573},[556,754,756,759,761],{"class":558,"line":755},22,[556,757,758],{"class":569},"          node-version",[556,760,574],{"class":573},[556,762,763],{"class":591},"20\n",[556,765,767,770,772,775],{"class":558,"line":766},23,[556,768,769],{"class":569},"          cache",[556,771,574],{"class":573},[556,773,774],{"class":577},"npm",[556,776,777],{"class":562},"            # cache the npm registry between runs\n",[556,779,781,783,786,788,791],{"class":558,"line":780},24,[556,782,724],{"class":573},[556,784,785],{"class":569},"run",[556,787,574],{"class":573},[556,789,790],{"class":577},"npm ci",[556,792,793],{"class":562},"             # exits non-zero on lockfile drift\n",[556,795,797,799,801,803,806],{"class":558,"line":796},25,[556,798,724],{"class":573},[556,800,785],{"class":569},[556,802,574],{"class":573},[556,804,805],{"class":577},"npm run build",[556,807,808],{"class":562},"      # build once; e2e + lighthouse reuse it\n",[556,810,812,814,816,818],{"class":558,"line":811},26,[556,813,724],{"class":573},[556,815,727],{"class":569},[556,817,574],{"class":573},[556,819,820],{"class":577},"actions\u002Fupload-artifact@v4\n",[556,822,824,826],{"class":558,"line":823},27,[556,825,750],{"class":569},[556,827,595],{"class":573},[556,829,831,834,836],{"class":558,"line":830},28,[556,832,833],{"class":569},"          name",[556,835,574],{"class":573},[556,837,838],{"class":577},"dist\n",[556,840,842,845,847],{"class":558,"line":841},29,[556,843,844],{"class":569},"          path",[556,846,574],{"class":573},[556,848,849],{"class":577},"dist\u002F\n",[556,851,853],{"class":558,"line":852},30,[556,854,585],{"emptyLinePlaceholder":584},[556,856,858,861],{"class":558,"line":857},31,[556,859,860],{"class":569},"  jest-axe",[556,862,595],{"class":573},[556,864,866,869,871],{"class":558,"line":865},32,[556,867,868],{"class":569},"    needs",[556,870,574],{"class":573},[556,872,873],{"class":577},"install\n",[556,875,877,879,881],{"class":558,"line":876},33,[556,878,705],{"class":569},[556,880,574],{"class":573},[556,882,710],{"class":577},[556,884,886,888],{"class":558,"line":885},34,[556,887,716],{"class":569},[556,889,595],{"class":573},[556,891,893,895,897,899],{"class":558,"line":892},35,[556,894,724],{"class":573},[556,896,727],{"class":569},[556,898,574],{"class":573},[556,900,732],{"class":577},[556,902,904,906,908,910],{"class":558,"line":903},36,[556,905,724],{"class":573},[556,907,727],{"class":569},[556,909,574],{"class":573},[556,911,744],{"class":577},[556,913,915,917,920,923,925,928,930,933,935,937],{"class":558,"line":914},37,[556,916,750],{"class":569},[556,918,919],{"class":573},": { ",[556,921,922],{"class":569},"node-version",[556,924,574],{"class":573},[556,926,927],{"class":591},"20",[556,929,337],{"class":573},[556,931,932],{"class":569},"cache",[556,934,574],{"class":573},[556,936,774],{"class":577},[556,938,939],{"class":573}," }\n",[556,941,943,945,947,949],{"class":558,"line":942},38,[556,944,724],{"class":573},[556,946,785],{"class":569},[556,948,574],{"class":573},[556,950,951],{"class":577},"npm ci\n",[556,953,955],{"class":558,"line":954},39,[556,956,957],{"class":562},"      # Fails the job (exit 1) the moment any axe violation is asserted.\n",[556,959,961,963,965,967],{"class":558,"line":960},40,[556,962,724],{"class":573},[556,964,785],{"class":569},[556,966,574],{"class":573},[556,968,969],{"class":577},"npm run test:a11y -- --ci\n",[556,971,973],{"class":558,"line":972},41,[556,974,585],{"emptyLinePlaceholder":584},[556,976,978,981],{"class":558,"line":977},42,[556,979,980],{"class":569},"  playwright-a11y",[556,982,595],{"class":573},[556,984,986,988,990],{"class":558,"line":985},43,[556,987,868],{"class":569},[556,989,574],{"class":573},[556,991,873],{"class":577},[556,993,995,997,999],{"class":558,"line":994},44,[556,996,705],{"class":569},[556,998,574],{"class":573},[556,1000,710],{"class":577},[556,1002,1004,1006],{"class":558,"line":1003},45,[556,1005,716],{"class":569},[556,1007,595],{"class":573},[556,1009,1011,1013,1015,1017],{"class":558,"line":1010},46,[556,1012,724],{"class":573},[556,1014,727],{"class":569},[556,1016,574],{"class":573},[556,1018,732],{"class":577},[556,1020,1022,1024,1026,1028],{"class":558,"line":1021},47,[556,1023,724],{"class":573},[556,1025,727],{"class":569},[556,1027,574],{"class":573},[556,1029,744],{"class":577},[556,1031,1033,1035,1037,1039,1041,1043,1045,1047,1049,1051],{"class":558,"line":1032},48,[556,1034,750],{"class":569},[556,1036,919],{"class":573},[556,1038,922],{"class":569},[556,1040,574],{"class":573},[556,1042,927],{"class":591},[556,1044,337],{"class":573},[556,1046,932],{"class":569},[556,1048,574],{"class":573},[556,1050,774],{"class":577},[556,1052,939],{"class":573},[556,1054,1056,1058,1060,1062],{"class":558,"line":1055},49,[556,1057,724],{"class":573},[556,1059,785],{"class":569},[556,1061,574],{"class":573},[556,1063,951],{"class":577},[556,1065,1067,1069,1071,1073],{"class":558,"line":1066},50,[556,1068,724],{"class":573},[556,1070,785],{"class":569},[556,1072,574],{"class":573},[556,1074,1075],{"class":577},"npx playwright install --with-deps chromium\n",[556,1077,1079,1081,1083,1085],{"class":558,"line":1078},51,[556,1080,724],{"class":573},[556,1082,727],{"class":569},[556,1084,574],{"class":573},[556,1086,1087],{"class":577},"actions\u002Fdownload-artifact@v4\n",[556,1089,1091,1093,1095,1097,1099,1102,1104,1107,1109,1112],{"class":558,"line":1090},52,[556,1092,750],{"class":569},[556,1094,919],{"class":573},[556,1096,570],{"class":569},[556,1098,574],{"class":573},[556,1100,1101],{"class":577},"dist",[556,1103,337],{"class":573},[556,1105,1106],{"class":569},"path",[556,1108,574],{"class":573},[556,1110,1111],{"class":577},"dist\u002F",[556,1113,939],{"class":573},[556,1115,1117,1119,1121,1123,1126],{"class":558,"line":1116},53,[556,1118,724],{"class":573},[556,1120,785],{"class":569},[556,1122,574],{"class":573},[556,1124,1125],{"class":577},"npm run test:e2e:a11y",[556,1127,1128],{"class":562},"       # non-zero on any violation\n",[556,1130,1132,1134,1136,1138],{"class":558,"line":1131},54,[556,1133,724],{"class":573},[556,1135,727],{"class":569},[556,1137,574],{"class":573},[556,1139,820],{"class":577},[556,1141,1143,1146,1148,1151],{"class":558,"line":1142},55,[556,1144,1145],{"class":569},"        if",[556,1147,574],{"class":573},[556,1149,1150],{"class":577},"always()",[556,1152,1153],{"class":562},"                     # keep the report even on failure\n",[556,1155,1157,1159],{"class":558,"line":1156},56,[556,1158,750],{"class":569},[556,1160,595],{"class":573},[556,1162,1164,1166,1168],{"class":558,"line":1163},57,[556,1165,833],{"class":569},[556,1167,574],{"class":573},[556,1169,1170],{"class":577},"playwright-a11y-report\n",[556,1172,1174,1176,1178],{"class":558,"line":1173},58,[556,1175,844],{"class":569},[556,1177,574],{"class":573},[556,1179,1180],{"class":577},"playwright-report\u002F\n",[556,1182,1184],{"class":558,"line":1183},59,[556,1185,585],{"emptyLinePlaceholder":584},[556,1187,1189,1192],{"class":558,"line":1188},60,[556,1190,1191],{"class":569},"  lighthouse-ci",[556,1193,595],{"class":573},[556,1195,1197,1199,1201],{"class":558,"line":1196},61,[556,1198,868],{"class":569},[556,1200,574],{"class":573},[556,1202,873],{"class":577},[556,1204,1206,1208,1210],{"class":558,"line":1205},62,[556,1207,705],{"class":569},[556,1209,574],{"class":573},[556,1211,710],{"class":577},[556,1213,1215,1217],{"class":558,"line":1214},63,[556,1216,716],{"class":569},[556,1218,595],{"class":573},[556,1220,1222,1224,1226,1228],{"class":558,"line":1221},64,[556,1223,724],{"class":573},[556,1225,727],{"class":569},[556,1227,574],{"class":573},[556,1229,732],{"class":577},[556,1231,1233,1235,1237,1239],{"class":558,"line":1232},65,[556,1234,724],{"class":573},[556,1236,727],{"class":569},[556,1238,574],{"class":573},[556,1240,744],{"class":577},[556,1242,1244,1246,1248,1250,1252,1254,1256,1258,1260,1262],{"class":558,"line":1243},66,[556,1245,750],{"class":569},[556,1247,919],{"class":573},[556,1249,922],{"class":569},[556,1251,574],{"class":573},[556,1253,927],{"class":591},[556,1255,337],{"class":573},[556,1257,932],{"class":569},[556,1259,574],{"class":573},[556,1261,774],{"class":577},[556,1263,939],{"class":573},[556,1265,1267,1269,1271,1273],{"class":558,"line":1266},67,[556,1268,724],{"class":573},[556,1270,785],{"class":569},[556,1272,574],{"class":573},[556,1274,951],{"class":577},[556,1276,1278,1280,1282,1284],{"class":558,"line":1277},68,[556,1279,724],{"class":573},[556,1281,727],{"class":569},[556,1283,574],{"class":573},[556,1285,1087],{"class":577},[556,1287,1289,1291,1293,1295,1297,1299,1301,1303,1305,1307],{"class":558,"line":1288},69,[556,1290,750],{"class":569},[556,1292,919],{"class":573},[556,1294,570],{"class":569},[556,1296,574],{"class":573},[556,1298,1101],{"class":577},[556,1300,337],{"class":573},[556,1302,1106],{"class":569},[556,1304,574],{"class":573},[556,1306,1111],{"class":577},[556,1308,939],{"class":573},[556,1310,1312],{"class":558,"line":1311},70,[556,1313,1314],{"class":562},"      # assertion failures in lighthouserc.js exit non-zero -> red check\n",[556,1316,1318,1320,1322,1324],{"class":558,"line":1317},71,[556,1319,724],{"class":573},[556,1321,785],{"class":569},[556,1323,574],{"class":573},[556,1325,1326],{"class":577},"npx @lhci\u002Fcli autorun\n",[324,1328,1329,1330,1333],{},"The matching ",[334,1331,1332],{},"package.json"," scripts make each command return the right exit code:",[547,1335,1339],{"className":1336,"code":1337,"language":1338,"meta":552,"style":552},"language-json shiki shiki-themes github-light github-dark","{\n  \"scripts\": {\n    \"test:a11y\": \"jest --selectProjects a11y\",\n    \"test:e2e:a11y\": \"playwright test --grep @a11y\",\n    \"lhci\": \"lhci autorun\"\n  }\n}\n","json",[334,1340,1341,1346,1354,1367,1379,1389,1394],{"__ignoreMap":552},[556,1342,1343],{"class":558,"line":559},[556,1344,1345],{"class":573},"{\n",[556,1347,1348,1351],{"class":558,"line":566},[556,1349,1350],{"class":591},"  \"scripts\"",[556,1352,1353],{"class":573},": {\n",[556,1355,1356,1359,1361,1364],{"class":558,"line":581},[556,1357,1358],{"class":591},"    \"test:a11y\"",[556,1360,574],{"class":573},[556,1362,1363],{"class":577},"\"jest --selectProjects a11y\"",[556,1365,1366],{"class":573},",\n",[556,1368,1369,1372,1374,1377],{"class":558,"line":588},[556,1370,1371],{"class":591},"    \"test:e2e:a11y\"",[556,1373,574],{"class":573},[556,1375,1376],{"class":577},"\"playwright test --grep @a11y\"",[556,1378,1366],{"class":573},[556,1380,1381,1384,1386],{"class":558,"line":598},[556,1382,1383],{"class":591},"    \"lhci\"",[556,1385,574],{"class":573},[556,1387,1388],{"class":577},"\"lhci autorun\"\n",[556,1390,1391],{"class":558,"line":606},[556,1392,1393],{"class":573},"  }\n",[556,1395,1396],{"class":558,"line":621},[556,1397,1398],{"class":573},"}\n",[324,1400,1401,1402,1405],{},"The Playwright accessibility spec asserts zero violations and lets the runner translate a failed ",[334,1403,1404],{},"expect"," into a non-zero exit:",[547,1407,1411],{"className":1408,"code":1409,"language":1410,"meta":552,"style":552},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F e2e\u002Fcheckout.a11y.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport AxeBuilder from '@axe-core\u002Fplaywright';\n\ntest('checkout route has no axe violations @a11y', async ({ page }) => {\n  await page.goto('\u002Fcheckout\u002F');\n  const results = await new AxeBuilder({ page })\n    .withTags(['wcag2a', 'wcag2aa']) \u002F\u002F gate on A + AA only\n    .analyze();\n  \u002F\u002F A non-empty violations array fails the test -> job exits non-zero.\n  expect(results.violations).toEqual([]);\n});\n","ts",[334,1412,1413,1418,1436,1450,1454,1487,1506,1529,1554,1564,1569,1583],{"__ignoreMap":552},[556,1414,1415],{"class":558,"line":559},[556,1416,1417],{"class":562},"\u002F\u002F e2e\u002Fcheckout.a11y.spec.ts\n",[556,1419,1420,1424,1427,1430,1433],{"class":558,"line":566},[556,1421,1423],{"class":1422},"szBVR","import",[556,1425,1426],{"class":573}," { test, expect } ",[556,1428,1429],{"class":1422},"from",[556,1431,1432],{"class":577}," '@playwright\u002Ftest'",[556,1434,1435],{"class":573},";\n",[556,1437,1438,1440,1443,1445,1448],{"class":558,"line":581},[556,1439,1423],{"class":1422},[556,1441,1442],{"class":573}," AxeBuilder ",[556,1444,1429],{"class":1422},[556,1446,1447],{"class":577}," '@axe-core\u002Fplaywright'",[556,1449,1435],{"class":573},[556,1451,1452],{"class":558,"line":588},[556,1453,585],{"emptyLinePlaceholder":584},[556,1455,1456,1460,1463,1466,1468,1471,1474,1478,1481,1484],{"class":558,"line":598},[556,1457,1459],{"class":1458},"sScJk","test",[556,1461,1462],{"class":573},"(",[556,1464,1465],{"class":577},"'checkout route has no axe violations @a11y'",[556,1467,337],{"class":573},[556,1469,1470],{"class":1422},"async",[556,1472,1473],{"class":573}," ({ ",[556,1475,1477],{"class":1476},"s4XuR","page",[556,1479,1480],{"class":573}," }) ",[556,1482,1483],{"class":1422},"=>",[556,1485,1486],{"class":573}," {\n",[556,1488,1489,1492,1495,1498,1500,1503],{"class":558,"line":606},[556,1490,1491],{"class":1422},"  await",[556,1493,1494],{"class":573}," page.",[556,1496,1497],{"class":1458},"goto",[556,1499,1462],{"class":573},[556,1501,1502],{"class":577},"'\u002Fcheckout\u002F'",[556,1504,1505],{"class":573},");\n",[556,1507,1508,1511,1514,1517,1520,1523,1526],{"class":558,"line":621},[556,1509,1510],{"class":1422},"  const",[556,1512,1513],{"class":591}," results",[556,1515,1516],{"class":1422}," =",[556,1518,1519],{"class":1422}," await",[556,1521,1522],{"class":1422}," new",[556,1524,1525],{"class":1458}," AxeBuilder",[556,1527,1528],{"class":573},"({ page })\n",[556,1530,1531,1534,1537,1540,1543,1545,1548,1551],{"class":558,"line":629},[556,1532,1533],{"class":573},"    .",[556,1535,1536],{"class":1458},"withTags",[556,1538,1539],{"class":573},"([",[556,1541,1542],{"class":577},"'wcag2a'",[556,1544,337],{"class":573},[556,1546,1547],{"class":577},"'wcag2aa'",[556,1549,1550],{"class":573},"]) ",[556,1552,1553],{"class":562},"\u002F\u002F gate on A + AA only\n",[556,1555,1556,1558,1561],{"class":558,"line":640},[556,1557,1533],{"class":573},[556,1559,1560],{"class":1458},"analyze",[556,1562,1563],{"class":573},"();\n",[556,1565,1566],{"class":558,"line":645},[556,1567,1568],{"class":562},"  \u002F\u002F A non-empty violations array fails the test -> job exits non-zero.\n",[556,1570,1571,1574,1577,1580],{"class":558,"line":651},[556,1572,1573],{"class":1458},"  expect",[556,1575,1576],{"class":573},"(results.violations).",[556,1578,1579],{"class":1458},"toEqual",[556,1581,1582],{"class":573},"([]);\n",[556,1584,1585],{"class":558,"line":659},[556,1586,1587],{"class":573},"});\n",[529,1589,1590],{},[324,1591,1592,1594,1595,1598],{},[343,1593,535],{}," Run ",[334,1596,1597],{},"withTags(['wcag2a', 'wcag2aa'])"," so the gate enforces a fixed conformance target. Pinning tags keeps the gate stable when axe-core ships new best-practice rules in a minor version bump.",[406,1600],{},[409,1602,1604],{"id":1603},"visualizing-the-gate","Visualizing the Gate",[324,1606,1607],{},"The diagram traces a pull request through the pipeline: a single red stage blocks the merge, and only an all-green tree satisfies branch protection.",[1609,1610,1617,1618,1617,1622,1617,1626,1617,1670,1617,1709,1617,1723,1617,1733],"svg",{"role":1611,"ariaLabelledBy":1612,"viewBox":1615,"style":1616},"img",[1613,1614],"gate-t","gate-d","0 0 760 250","width:100%;height:auto;max-width:760px","\n  ",[1619,1620,1621],"title",{"id":1613},"Accessibility CI gate pipeline",[1623,1624,1625],"desc",{"id":1614},"A pull request flows through install, jest-axe, Playwright accessibility, and Lighthouse CI stages. All stages must pass green for branch protection to allow a merge; any red stage blocks the merge.",[1627,1628,1633,1634,1633,1643,1633,1647,1633,1650,1633,1653,1633,1657,1633,1661,1633,1664,1633,1667,1617],"g",{"style":1629,"fill":1630,"stroke":1631,"color":1632},"stroke-width:2","none","currentColor","var(--primary)","\n    ",[1635,1636],"rect",{"x":1637,"y":1638,"width":1639,"height":1640,"rx":1641,"fill":1642},"12","100","118","50","8","var(--primary-soft)",[1635,1644],{"x":1645,"y":1638,"width":1639,"height":1640,"rx":1641,"fill":1646},"160","var(--surface)",[1635,1648],{"x":1649,"y":1638,"width":1639,"height":1640,"rx":1641,"fill":1646},"308",[1635,1651],{"x":1652,"y":1638,"width":1639,"height":1640,"rx":1641,"fill":1646},"456",[1635,1654],{"x":1655,"y":1638,"width":1656,"height":1640,"rx":1641,"fill":1642},"604","144",[558,1658],{"x1":1659,"y1":1660,"x2":1645,"y2":1660},"130","125",[558,1662],{"x1":1663,"y1":1660,"x2":1649,"y2":1660},"278",[558,1665],{"x1":1666,"y1":1660,"x2":1652,"y2":1660},"426",[558,1668],{"x1":1669,"y1":1660,"x2":1655,"y2":1660},"574",[1627,1671,1633,1674,1633,1680,1633,1684,1633,1688,1633,1692,1633,1695,1633,1699,1633,1702,1633,1706,1617],{"style":1672,"fill":1673},"font-size:13px;text-anchor:middle","var(--text)",[1675,1676,1679],"text",{"x":1677,"y":1678},"71","122","PR opened",[1675,1681,1683],{"x":1677,"y":1682},"140","+ install",[1675,1685,340],{"x":1686,"y":1687},"219","129",[1675,1689,1691],{"x":1690,"y":1678},"367","Playwright",[1675,1693,1694],{"x":1690,"y":1682},"a11y",[1675,1696,1698],{"x":1697,"y":1678},"515","Lighthouse",[1675,1700,1701],{"x":1697,"y":1682},"CI",[1675,1703,1705],{"x":1704,"y":1678},"676","All green",[1675,1707,1708],{"x":1704,"y":1682},"→ merge",[1627,1710,1633,1713,1633,1717,1633,1720,1617],{"style":1711,"fill":1712},"font-size:12px;text-anchor:middle","var(--muted)",[1675,1714,1716],{"x":1686,"y":1715},"180","unit",[1675,1718,1719],{"x":1690,"y":1715},"browser e2e",[1675,1721,1722],{"x":1697,"y":1715},"page budget",[1627,1724,1633,1725,1633,1729,1617],{"style":1711,"fill":1673},[1675,1726,1728],{"x":1690,"y":1727},"40","any red stage",[1675,1730,1732],{"x":1690,"y":1731},"58","blocks the merge",[1627,1734,1633,1736,1633,1739,1633,1741,1617],{"style":1629,"stroke":1631,"color":1735},"var(--primary-strong)",[558,1737],{"x1":1686,"y1":1738,"x2":1686,"y2":1638},"68",[558,1740],{"x1":1690,"y1":1738,"x2":1690,"y2":1638},[558,1742],{"x1":1697,"y1":1738,"x2":1697,"y2":1638},[406,1744],{},[409,1746,1748],{"id":1747},"making-the-checks-required-via-branch-protection","Making the Checks Required via Branch Protection",[324,1750,1751,1752,1755,1756,1758,1759,337,1761,337,1764,1767],{},"A passing or failing job means nothing until GitHub treats it as ",[343,1753,1754],{},"required",". A green log that does not block a merge is documentation, not a gate. Configure a branch protection rule (or a repository ruleset) on ",[334,1757,615],{}," so the three job names are required status checks. Use the exact job key—",[334,1760,340],{},[334,1762,1763],{},"playwright-a11y",[334,1765,1766],{},"lighthouse-ci","—not the workflow name.",[547,1769,1771],{"className":549,"code":1770,"language":551,"meta":552,"style":552},"# Repository ruleset (Settings → Rules → Rulesets), exported as JSON-equivalent YAML\ntarget: branch\nconditions:\n  ref_name:\n    include: [\"refs\u002Fheads\u002Fmain\"]\nrules:\n  - type: pull_request\n    parameters:\n      required_approving_review_count: 1\n  - type: required_status_checks\n    parameters:\n      strict_required_status_checks_policy: true   # branch must be up to date\n      required_status_checks:\n        - context: jest-axe\n        - context: playwright-a11y\n        - context: lighthouse-ci\n",[334,1772,1773,1778,1788,1795,1802,1814,1821,1834,1841,1851,1862,1868,1881,1888,1901,1912],{"__ignoreMap":552},[556,1774,1775],{"class":558,"line":559},[556,1776,1777],{"class":562},"# Repository ruleset (Settings → Rules → Rulesets), exported as JSON-equivalent YAML\n",[556,1779,1780,1783,1785],{"class":558,"line":566},[556,1781,1782],{"class":569},"target",[556,1784,574],{"class":573},[556,1786,1787],{"class":577},"branch\n",[556,1789,1790,1793],{"class":558,"line":581},[556,1791,1792],{"class":569},"conditions",[556,1794,595],{"class":573},[556,1796,1797,1800],{"class":558,"line":588},[556,1798,1799],{"class":569},"  ref_name",[556,1801,595],{"class":573},[556,1803,1804,1807,1809,1812],{"class":558,"line":598},[556,1805,1806],{"class":569},"    include",[556,1808,612],{"class":573},[556,1810,1811],{"class":577},"\"refs\u002Fheads\u002Fmain\"",[556,1813,618],{"class":573},[556,1815,1816,1819],{"class":558,"line":606},[556,1817,1818],{"class":569},"rules",[556,1820,595],{"class":573},[556,1822,1823,1826,1829,1831],{"class":558,"line":621},[556,1824,1825],{"class":573},"  - ",[556,1827,1828],{"class":569},"type",[556,1830,574],{"class":573},[556,1832,1833],{"class":577},"pull_request\n",[556,1835,1836,1839],{"class":558,"line":629},[556,1837,1838],{"class":569},"    parameters",[556,1840,595],{"class":573},[556,1842,1843,1846,1848],{"class":558,"line":640},[556,1844,1845],{"class":569},"      required_approving_review_count",[556,1847,574],{"class":573},[556,1849,1850],{"class":591},"1\n",[556,1852,1853,1855,1857,1859],{"class":558,"line":645},[556,1854,1825],{"class":573},[556,1856,1828],{"class":569},[556,1858,574],{"class":573},[556,1860,1861],{"class":577},"required_status_checks\n",[556,1863,1864,1866],{"class":558,"line":651},[556,1865,1838],{"class":569},[556,1867,595],{"class":573},[556,1869,1870,1873,1875,1878],{"class":558,"line":659},[556,1871,1872],{"class":569},"      strict_required_status_checks_policy",[556,1874,574],{"class":573},[556,1876,1877],{"class":591},"true",[556,1879,1880],{"class":562},"   # branch must be up to date\n",[556,1882,1883,1886],{"class":558,"line":670},[556,1884,1885],{"class":569},"      required_status_checks",[556,1887,595],{"class":573},[556,1889,1890,1893,1896,1898],{"class":558,"line":681},[556,1891,1892],{"class":573},"        - ",[556,1894,1895],{"class":569},"context",[556,1897,574],{"class":573},[556,1899,1900],{"class":577},"jest-axe\n",[556,1902,1903,1905,1907,1909],{"class":558,"line":686},[556,1904,1892],{"class":573},[556,1906,1895],{"class":569},[556,1908,574],{"class":573},[556,1910,1911],{"class":577},"playwright-a11y\n",[556,1913,1914,1916,1918,1920],{"class":558,"line":694},[556,1915,1892],{"class":573},[556,1917,1895],{"class":569},[556,1919,574],{"class":573},[556,1921,1922],{"class":577},"lighthouse-ci\n",[324,1924,1925,1926,1929,1930,527],{},"Enable ",[343,1927,1928],{},"strict"," required checks so a PR cannot merge against a stale base—this prevents a regression sneaking in through a branch that was green before a conflicting change landed. The mechanics of wiring a single job into a required check are detailed in ",[328,1931,309],{"href":1932},"\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations\u002F",[529,1934,1935],{},[324,1936,1937,1939,1940,1943],{},[343,1938,535],{}," Required checks only fire if the workflow actually runs on the PR. If a path filter skips the workflow, the required check never reports and the PR blocks forever. Use a no-op fallback job or avoid ",[334,1941,1942],{},"paths:"," filters on gated workflows.",[406,1945],{},[409,1947,1949],{"id":1948},"annotating-prs-with-results-and-artifacts","Annotating PRs with Results and Artifacts",[324,1951,1952,1953,1957,1958,1960,1961,1964],{},"A red check is necessary but not sufficient—engineers need to see ",[1954,1955,1956],"em",{},"which"," node failed ",[1954,1959,1956],{}," rule without digging through raw logs. Upload the Playwright HTML report and the Lighthouse report as artifacts (shown above with ",[334,1962,1963],{},"if: always()","), and surface a summary directly on the PR using the GitHub step summary and check annotations.",[547,1966,1968],{"className":549,"code":1967,"language":551,"meta":552,"style":552},"  - name: Summarize axe violations\n    if: always()\n    run: |\n      # Emit a Markdown table into the PR's job summary panel.\n      node .\u002Fscripts\u002Fformat-axe-summary.js >> \"$GITHUB_STEP_SUMMARY\"\n",[334,1969,1970,1981,1991,2001,2006],{"__ignoreMap":552},[556,1971,1972,1974,1976,1978],{"class":558,"line":559},[556,1973,1825],{"class":573},[556,1975,570],{"class":569},[556,1977,574],{"class":573},[556,1979,1980],{"class":577},"Summarize axe violations\n",[556,1982,1983,1986,1988],{"class":558,"line":566},[556,1984,1985],{"class":569},"    if",[556,1987,574],{"class":573},[556,1989,1990],{"class":577},"always()\n",[556,1992,1993,1996,1998],{"class":558,"line":581},[556,1994,1995],{"class":569},"    run",[556,1997,574],{"class":573},[556,1999,2000],{"class":1422},"|\n",[556,2002,2003],{"class":558,"line":588},[556,2004,2005],{"class":577},"      # Emit a Markdown table into the PR's job summary panel.\n",[556,2007,2008],{"class":558,"line":598},[556,2009,2010],{"class":577},"      node .\u002Fscripts\u002Fformat-axe-summary.js >> \"$GITHUB_STEP_SUMMARY\"\n",[547,2012,2016],{"className":2013,"code":2014,"language":2015,"meta":552,"style":552},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F scripts\u002Fformat-axe-summary.js — turns saved axe JSON into a PR table\nconst results = require('..\u002Fa11y-results.json');\nconsole.log('| Rule | Impact | Selector |');\nconsole.log('| --- | --- | --- |');\nfor (const v of results.violations) {\n  for (const node of v.nodes) {\n    console.log(`| ${v.id} | ${v.impact} | \\`${node.target.join(' ')}\\` |`);\n  }\n}\n\u002F\u002F Exit non-zero so this step also reflects the gate state.\nprocess.exit(results.violations.length ? 1 : 0);\n","js",[334,2017,2018,2023,2042,2057,2070,2089,2106,2175,2179,2183,2188],{"__ignoreMap":552},[556,2019,2020],{"class":558,"line":559},[556,2021,2022],{"class":562},"\u002F\u002F scripts\u002Fformat-axe-summary.js — turns saved axe JSON into a PR table\n",[556,2024,2025,2028,2030,2032,2035,2037,2040],{"class":558,"line":566},[556,2026,2027],{"class":1422},"const",[556,2029,1513],{"class":591},[556,2031,1516],{"class":1422},[556,2033,2034],{"class":1458}," require",[556,2036,1462],{"class":573},[556,2038,2039],{"class":577},"'..\u002Fa11y-results.json'",[556,2041,1505],{"class":573},[556,2043,2044,2047,2050,2052,2055],{"class":558,"line":581},[556,2045,2046],{"class":573},"console.",[556,2048,2049],{"class":1458},"log",[556,2051,1462],{"class":573},[556,2053,2054],{"class":577},"'| Rule | Impact | Selector |'",[556,2056,1505],{"class":573},[556,2058,2059,2061,2063,2065,2068],{"class":558,"line":588},[556,2060,2046],{"class":573},[556,2062,2049],{"class":1458},[556,2064,1462],{"class":573},[556,2066,2067],{"class":577},"'| --- | --- | --- |'",[556,2069,1505],{"class":573},[556,2071,2072,2075,2078,2080,2083,2086],{"class":558,"line":598},[556,2073,2074],{"class":1422},"for",[556,2076,2077],{"class":573}," (",[556,2079,2027],{"class":1422},[556,2081,2082],{"class":591}," v",[556,2084,2085],{"class":1422}," of",[556,2087,2088],{"class":573}," results.violations) {\n",[556,2090,2091,2094,2096,2098,2101,2103],{"class":558,"line":606},[556,2092,2093],{"class":1422},"  for",[556,2095,2077],{"class":573},[556,2097,2027],{"class":1422},[556,2099,2100],{"class":591}," node",[556,2102,2085],{"class":1422},[556,2104,2105],{"class":573}," v.nodes) {\n",[556,2107,2108,2111,2113,2115,2118,2121,2123,2126,2129,2131,2133,2136,2139,2142,2145,2148,2150,2152,2154,2157,2159,2162,2165,2168,2170,2173],{"class":558,"line":621},[556,2109,2110],{"class":573},"    console.",[556,2112,2049],{"class":1458},[556,2114,1462],{"class":573},[556,2116,2117],{"class":577},"`| ${",[556,2119,2120],{"class":573},"v",[556,2122,527],{"class":577},[556,2124,2125],{"class":573},"id",[556,2127,2128],{"class":577},"} | ${",[556,2130,2120],{"class":573},[556,2132,527],{"class":577},[556,2134,2135],{"class":573},"impact",[556,2137,2138],{"class":577},"} | ",[556,2140,2141],{"class":591},"\\`",[556,2143,2144],{"class":577},"${",[556,2146,2147],{"class":573},"node",[556,2149,527],{"class":577},[556,2151,1782],{"class":573},[556,2153,527],{"class":577},[556,2155,2156],{"class":1458},"join",[556,2158,1462],{"class":577},[556,2160,2161],{"class":577},"' '",[556,2163,2164],{"class":577},")",[556,2166,2167],{"class":577},"}",[556,2169,2141],{"class":591},[556,2171,2172],{"class":577}," |`",[556,2174,1505],{"class":573},[556,2176,2177],{"class":558,"line":629},[556,2178,1393],{"class":573},[556,2180,2181],{"class":558,"line":640},[556,2182,1398],{"class":573},[556,2184,2185],{"class":558,"line":645},[556,2186,2187],{"class":562},"\u002F\u002F Exit non-zero so this step also reflects the gate state.\n",[556,2189,2190,2193,2196,2199,2202,2205,2208,2211,2214],{"class":558,"line":651},[556,2191,2192],{"class":573},"process.",[556,2194,2195],{"class":1458},"exit",[556,2197,2198],{"class":573},"(results.violations.",[556,2200,2201],{"class":591},"length",[556,2203,2204],{"class":1422}," ?",[556,2206,2207],{"class":591}," 1",[556,2209,2210],{"class":1422}," :",[556,2212,2213],{"class":591}," 0",[556,2215,1505],{"class":573},[324,2217,2218,2219,2222,2223,2225],{},"Writing to ",[334,2220,2221],{},"$GITHUB_STEP_SUMMARY"," renders a Markdown panel on the run, so reviewers see the offending ",[334,2224,349],{}," selector inline. Pair this with the downloadable HTML report for the full DOM context.",[406,2227],{},[409,2229,2231],{"id":2230},"baseline-and-allowlist-for-legacy-debt","Baseline and Allowlist for Legacy Debt",[324,2233,2234,2235,2238,2239,2242],{},"A gate that fails on day one against an existing codebase gets disabled within a week. Ship it incrementally with a ",[343,2236,2237],{},"baseline",": snapshot the currently accepted violations, fail only on ",[1954,2240,2241],{},"new"," ones, and burn the list down over time. This is the difference between a gate teams keep and one they bypass.",[547,2244,2246],{"className":2013,"code":2245,"language":2015,"meta":552,"style":552},"\u002F\u002F a11y-baseline.js — known, triaged debt that does NOT fail the build yet\nmodule.exports = {\n  \u002F\u002F Each entry: rule id + a stable selector signature.\n  allow: [\n    { rule: 'color-contrast', selector: '.legacy-banner .cta' },\n    { rule: 'aria-required-children', selector: '#old-grid' },\n  ],\n};\n",[334,2247,2248,2253,2267,2272,2277,2294,2308,2313],{"__ignoreMap":552},[556,2249,2250],{"class":558,"line":559},[556,2251,2252],{"class":562},"\u002F\u002F a11y-baseline.js — known, triaged debt that does NOT fail the build yet\n",[556,2254,2255,2258,2260,2263,2265],{"class":558,"line":566},[556,2256,2257],{"class":591},"module",[556,2259,527],{"class":573},[556,2261,2262],{"class":591},"exports",[556,2264,1516],{"class":1422},[556,2266,1486],{"class":573},[556,2268,2269],{"class":558,"line":581},[556,2270,2271],{"class":562},"  \u002F\u002F Each entry: rule id + a stable selector signature.\n",[556,2273,2274],{"class":558,"line":588},[556,2275,2276],{"class":573},"  allow: [\n",[556,2278,2279,2282,2285,2288,2291],{"class":558,"line":598},[556,2280,2281],{"class":573},"    { rule: ",[556,2283,2284],{"class":577},"'color-contrast'",[556,2286,2287],{"class":573},", selector: ",[556,2289,2290],{"class":577},"'.legacy-banner .cta'",[556,2292,2293],{"class":573}," },\n",[556,2295,2296,2298,2301,2303,2306],{"class":558,"line":606},[556,2297,2281],{"class":573},[556,2299,2300],{"class":577},"'aria-required-children'",[556,2302,2287],{"class":573},[556,2304,2305],{"class":577},"'#old-grid'",[556,2307,2293],{"class":573},[556,2309,2310],{"class":558,"line":621},[556,2311,2312],{"class":573},"  ],\n",[556,2314,2315],{"class":558,"line":629},[556,2316,2317],{"class":573},"};\n",[547,2319,2321],{"className":2013,"code":2320,"language":2015,"meta":552,"style":552},"\u002F\u002F jest setup — subtract baseline before asserting\nimport { baseline } from '.\u002Fa11y-baseline';\n\nexport function expectNoNewViolations(results) {\n  const fresh = results.violations.filter(\n    (v) => !baseline.allow.some((b) => b.rule === v.id),\n  );\n  \u002F\u002F Only NEW violations fail; baselined debt is tracked, not blocking.\n  expect(fresh).toEqual([]);\n}\n",[334,2322,2323,2328,2342,2346,2365,2383,2423,2428,2433,2444],{"__ignoreMap":552},[556,2324,2325],{"class":558,"line":559},[556,2326,2327],{"class":562},"\u002F\u002F jest setup — subtract baseline before asserting\n",[556,2329,2330,2332,2335,2337,2340],{"class":558,"line":566},[556,2331,1423],{"class":1422},[556,2333,2334],{"class":573}," { baseline } ",[556,2336,1429],{"class":1422},[556,2338,2339],{"class":577}," '.\u002Fa11y-baseline'",[556,2341,1435],{"class":573},[556,2343,2344],{"class":558,"line":581},[556,2345,585],{"emptyLinePlaceholder":584},[556,2347,2348,2351,2354,2357,2359,2362],{"class":558,"line":588},[556,2349,2350],{"class":1422},"export",[556,2352,2353],{"class":1422}," function",[556,2355,2356],{"class":1458}," expectNoNewViolations",[556,2358,1462],{"class":573},[556,2360,2361],{"class":1476},"results",[556,2363,2364],{"class":573},") {\n",[556,2366,2367,2369,2372,2374,2377,2380],{"class":558,"line":598},[556,2368,1510],{"class":1422},[556,2370,2371],{"class":591}," fresh",[556,2373,1516],{"class":1422},[556,2375,2376],{"class":573}," results.violations.",[556,2378,2379],{"class":1458},"filter",[556,2381,2382],{"class":573},"(\n",[556,2384,2385,2388,2390,2393,2395,2398,2401,2404,2407,2410,2412,2414,2417,2420],{"class":558,"line":606},[556,2386,2387],{"class":573},"    (",[556,2389,2120],{"class":1476},[556,2391,2392],{"class":573},") ",[556,2394,1483],{"class":1422},[556,2396,2397],{"class":1422}," !",[556,2399,2400],{"class":573},"baseline.allow.",[556,2402,2403],{"class":1458},"some",[556,2405,2406],{"class":573},"((",[556,2408,2409],{"class":1476},"b",[556,2411,2392],{"class":573},[556,2413,1483],{"class":1422},[556,2415,2416],{"class":573}," b.rule ",[556,2418,2419],{"class":1422},"===",[556,2421,2422],{"class":573}," v.id),\n",[556,2424,2425],{"class":558,"line":621},[556,2426,2427],{"class":573},"  );\n",[556,2429,2430],{"class":558,"line":629},[556,2431,2432],{"class":562},"  \u002F\u002F Only NEW violations fail; baselined debt is tracked, not blocking.\n",[556,2434,2435,2437,2440,2442],{"class":558,"line":640},[556,2436,1573],{"class":1458},[556,2438,2439],{"class":573},"(fresh).",[556,2441,1579],{"class":1458},[556,2443,1582],{"class":573},[556,2445,2446],{"class":558,"line":645},[556,2447,1398],{"class":573},[324,2449,2450,2451,527],{},"Keep each allowlist entry narrow—scope it to a rule plus a specific selector, never a blanket disable—so a brand-new contrast failure elsewhere still fails the gate. The full baseline-and-diff workflow, including scheduled audits that surface the remaining debt, is covered in ",[328,2452,303],{"href":2453},"\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions\u002F",[406,2455],{},[409,2457,2459],{"id":2458},"keeping-the-suite-fast-and-non-flaky","Keeping the Suite Fast and Non-Flaky",[324,2461,2462],{},"A slow or flaky gate trains engineers to re-run until green, which destroys the signal. Keep total wall-clock under a few minutes and keep failures real.",[361,2464,2465,2471,2477,2490,2496],{},[364,2466,2467,2470],{},[343,2468,2469],{},"Cache aggressively."," Cache npm and the Playwright browser binaries so installs do not dominate runtime.",[364,2472,2473,2476],{},[343,2474,2475],{},"Shard Playwright"," across runners with a matrix when the e2e suite grows past a minute.",[364,2478,2479,2482,2483,2485,2486,2489],{},[343,2480,2481],{},"Wait on conditions, not timers."," Use Playwright's auto-waiting and ",[334,2484,1404],{}," polling instead of ",[334,2487,2488],{},"waitForTimeout",", which is the top source of e2e flake.",[364,2491,2492,2495],{},[343,2493,2494],{},"Pin axe-core"," to an exact version. A minor bump that adds rules can fail a previously green build; upgrade deliberately.",[364,2497,2498,2501,2502,2504],{},[343,2499,2500],{},"Fail fast."," The ",[334,2503,340],{}," job runs first and cheapest, so most regressions never reach the browser stage.",[547,2506,2508],{"className":549,"code":2507,"language":551,"meta":552,"style":552},"  - uses: actions\u002Fcache@v4\n    with:\n      path: ~\u002F.cache\u002Fms-playwright    # reuse browser binaries across runs\n      key: pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n",[334,2509,2510,2521,2528,2541],{"__ignoreMap":552},[556,2511,2512,2514,2516,2518],{"class":558,"line":559},[556,2513,1825],{"class":573},[556,2515,727],{"class":569},[556,2517,574],{"class":573},[556,2519,2520],{"class":577},"actions\u002Fcache@v4\n",[556,2522,2523,2526],{"class":558,"line":566},[556,2524,2525],{"class":569},"    with",[556,2527,595],{"class":573},[556,2529,2530,2533,2535,2538],{"class":558,"line":581},[556,2531,2532],{"class":569},"      path",[556,2534,574],{"class":573},[556,2536,2537],{"class":577},"~\u002F.cache\u002Fms-playwright",[556,2539,2540],{"class":562},"    # reuse browser binaries across runs\n",[556,2542,2543,2546,2548],{"class":558,"line":588},[556,2544,2545],{"class":569},"      key",[556,2547,574],{"class":573},[556,2549,2550],{"class":577},"pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n",[529,2552,2553],{},[324,2554,2555,2557,2558,2561],{},[343,2556,535],{}," Quarantine a genuinely flaky test by tagging it ",[334,2559,2560],{},"@flaky"," and excluding it from the required job—never disable the whole stage. A scoped quarantine keeps the gate green-meaningful while you fix the root cause.",[406,2563],{},[409,2565,2567],{"id":2566},"key-takeaways","Key Takeaways",[361,2569,2570,2576,2579,2586,2589,2592],{},[364,2571,2572,2573,2575],{},"Layer the gate: ",[334,2574,340],{}," (unit) → Playwright (browser e2e) → Lighthouse CI (page budget), fastest first.",[364,2577,2578],{},"A non-zero exit code is the contract; branch protection converts it into a real merge block.",[364,2580,2581,2582,2585],{},"Make each job a ",[343,2583,2584],{},"required status check"," with strict up-to-date enforcement.",[364,2587,2588],{},"Surface failing nodes on the PR via step summaries and uploaded HTML reports.",[364,2590,2591],{},"Ship incrementally with a scoped baseline so legacy debt never blocks adoption.",[364,2593,2594],{},"Cache, shard, and wait-on-conditions to keep the suite fast and trustworthy.",[406,2596],{},[409,2598,2600],{"id":2599},"frequently-asked-questions","Frequently Asked Questions",[324,2602,2603,2606,2607,350,2609,2611],{},[343,2604,2605],{},"Should accessibility tests block a merge or just warn?","\nBlock. A warning that does not stop a merge is ignored within a sprint. Configure the jobs as required status checks under branch protection so a ",[334,2608,468],{},[334,2610,489],{}," violation returns a non-zero exit code and the PR cannot merge until it is fixed or explicitly baselined.",[324,2613,2614,2617,2618,2620],{},[343,2615,2616],{},"Won't a strict gate block adoption on a legacy codebase with existing violations?","\nNot if you ship it with a baseline. Snapshot the current accepted violations into an allowlist, fail only on ",[1954,2619,2241],{}," violations, and burn down the list over time. Teams adopt a gate that protects new code far more readily than one that fails on day one.",[324,2622,2623,2626,2628],{},[343,2624,2625],{},"Which tool should run first in the pipeline?",[334,2627,340],{},", because it is the fastest and cheapest. It catches missing labels, invalid ARIA, and broken name\u002Frole\u002Fvalue contracts in jsdom in seconds, failing the build before you spend CI minutes spinning up browsers for Playwright or Lighthouse.",[324,2630,2631,2634],{},[343,2632,2633],{},"Does this CI gate replace manual screen reader testing?","\nNo. Automated checks enforce the structural floor—valid roles, names, contrast, and keyboard reachability—but cannot judge announcement quality, reading order nuance, or speech verbosity. Keep manual NVDA, JAWS, and VoiceOver passes for high-interaction flows.",[324,2636,2637,2640,2641,2643,2644,2646],{},[343,2638,2639],{},"How do I keep the e2e stage from becoming flaky?","\nRely on Playwright's auto-waiting and ",[334,2642,1404],{}," polling rather than fixed ",[334,2645,2488],{}," calls, cache the browser binaries, pin axe-core to an exact version, and quarantine any genuinely flaky spec with a tag instead of disabling the whole required job.",[324,2648,2649,2652,2653,2655],{},[343,2650,2651],{},"How many WCAG levels should the gate enforce?","\nGate on A and AA using ",[334,2654,1597],{},". Pinning the tag set keeps the gate stable when axe-core adds new best-practice rules, and AA is the conformance target most legal and procurement requirements reference.",[406,2657],{},[409,2659,2661],{"id":2660},"related-guides","Related guides",[361,2663,2664,2668,2672,2676,2680,2684],{},[364,2665,2666],{},[328,2667,331],{"href":330},[364,2669,2670],{},[328,2671,279],{"href":522},[364,2673,2674],{},[328,2675,225],{"href":526},[364,2677,2678],{},[328,2679,261],{"href":518},[364,2681,2682],{},[328,2683,309],{"href":1932},[364,2685,2686],{},[328,2687,303],{"href":2453},[2689,2690,2691],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":552,"searchDepth":566,"depth":566,"links":2693},[2694,2695,2696,2697,2698,2699,2700,2701,2702,2703],{"id":411,"depth":566,"text":412},{"id":541,"depth":566,"text":542},{"id":1603,"depth":566,"text":1604},{"id":1747,"depth":566,"text":1748},{"id":1948,"depth":566,"text":1949},{"id":2230,"depth":566,"text":2231},{"id":2458,"depth":566,"text":2459},{"id":2566,"depth":566,"text":2567},{"id":2599,"depth":566,"text":2600},{"id":2660,"depth":566,"text":2661},null,"Make accessibility a release gate—wire axe, jest-axe, Playwright, and Lighthouse into GitHub Actions so violations fail the build and regressions can't reach production.","md",{},false,{"title":297,"description":2705},"UM08D0vrme5YmuToEqc-lugfA_PVJBUcAWXeLEcIgUo",[2712,2751,2752,2815],{"title":5,"path":6,"stem":7,"children":2713},[2714,2715,2718,2721,2727,2733,2742,2748],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2716},[2717],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2719},[2720],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2722},[2723,2724],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2725},[2726],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2728},[2729,2730],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2731},[2732],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2734},[2735,2736,2739],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2737},[2738],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2740},[2741],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2743},[2744,2745],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2746},[2747],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2749},[2750],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2753},[2754,2755,2761,2773,2785,2788,2797,2809],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2756},[2757,2758],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2759},[2760],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2762},[2763,2764,2767,2770],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2765},[2766],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2768},[2769],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2771},[2772],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2774},[2775,2776,2779,2782],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2777},[2778],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2780},[2781],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2783},[2784],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2786},[2787],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2789},[2790,2791,2794],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2792},[2793],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2795},[2796],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2798},[2799,2800,2803,2806],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2801},[2802],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2804},[2805],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2807},[2808],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2810},[2811,2812],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2813},[2814],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2816},[2817,2818,2827,2836,2845,2854],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2819},[2820,2821,2824],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2822},[2823],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2825},[2826],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2828},[2829,2830,2833],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2831},[2832],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2834},[2835],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2837},[2838,2839,2842],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2840},[2841],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2843},[2844],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2846},[2847,2848,2851],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2849},[2850],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2852},[2853],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2855},[2856,2857,2860],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2858},[2859],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2861},[2862],{"title":309,"path":310,"stem":311},1781785523930]