[{"data":1,"prerenderedAt":2118},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002F":314,"content-navigation":1966},[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":261,"body":316,"date":1959,"description":1960,"extension":1961,"image":1959,"meta":1962,"modifiedAt":1959,"navigation":566,"noindex":1963,"path":262,"publishedAt":1959,"seo":1964,"stem":263,"updatedAt":1959,"__hash__":1965},"content\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Findex.md",{"type":317,"value":318,"toc":1948},"minimark",[319,323,349,360,474,477,482,498,616,675,685,687,691,708,886,926,940,942,946,951,1081,1084,1128,1134,1136,1140,1143,1422,1445,1447,1451,1472,1688,1701,1703,1707,1710,1753,1759,1775,1777,1781,1832,1834,1838,1865,1877,1889,1902,1911,1913,1917,1944],[320,321,261],"h1",{"id":322},"component-testing-with-jest-axe",[324,325,326,330,331,336,337,340,341,344,345,348],"p",{},[327,328,329],"code",{},"jest-axe"," runs the axe-core rules engine against the DOM your React component renders, so accessibility violations fail your unit suite the same way a broken assertion does. Wired into Testing Library, it turns the abstract goal of \"accessible components\" into a concrete, fast, per-component gate that runs on every commit. This guide sits under ",[332,333,335],"a",{"href":334},"\u002Ftesting-and-automating-accessibility\u002F","Testing and Automating Accessibility"," and focuses on the component layer: render a component, run axe against its container, assert zero violations, and then test the dynamic states—open modals, error forms, expanded menus—where regressions actually hide. Axe-core enforces machine-checkable subsets of ",[327,338,339],{},"1.3.1 Info and Relationships",", ",[327,342,343],{},"4.1.2 Name, Role, Value",", and ",[327,346,347],{},"3.3.2 Labels or Instructions",", while you pair it with role\u002Fname queries to assert the contracts axe cannot evaluate on its own.",[324,350,351,352,355,356,359],{},"The trade-off to understand up front: jest-axe runs inside jsdom, which has no layout engine and no rendering. That makes it ideal for structural rules (missing labels, broken ",[327,353,354],{},"aria-*"," references, invalid roles) and useless for anything requiring computed geometry or color—those checks belong in ",[332,357,279],{"href":358},"\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002F",". Knowing which layer owns which rule is the difference between a green suite that means something and one that lies.",[361,362,369,370,369,374,369,378,369,427,369,454,369,461],"svg",{"role":363,"ariaLabelledBy":364,"viewBox":367,"style":368},"img",[365,366],"flow-t","flow-d","0 0 760 220","width:100%;height:auto;max-width:760px","\n  ",[371,372,373],"title",{"id":365},"jest-axe component test flow",[375,376,377],"desc",{"id":366},"A pipeline: render the component, run axe against the container, assert no violations, then gate the result in CI. A side branch shows layout and color rules being deferred to Playwright because jsdom has no rendering engine.",[379,380,384,385,384,394,384,397,384,400,384,404,384,411,384,416,384,419,384,422,369],"g",{"style":381,"fill":382,"stroke":383},"stroke-width:2","none","currentColor","\n    ",[386,387],"rect",{"x":388,"y":389,"width":390,"height":391,"rx":392,"fill":393},"16","40","150","64","8","var(--primary-soft)",[386,395],{"x":396,"y":389,"width":390,"height":391,"rx":392,"fill":393},"206",[386,398],{"x":399,"y":389,"width":390,"height":391,"rx":392,"fill":393},"396",[386,401],{"x":402,"y":389,"width":390,"height":391,"rx":392,"fill":403},"586","var(--primary)",[386,405],{"x":399,"y":406,"width":407,"height":408,"rx":392,"fill":409,"stroke":410},"148","340","52","var(--surface)","var(--border)",[412,413],"line",{"x1":414,"y1":415,"x2":396,"y2":415},"166","72",[412,417],{"x1":418,"y1":415,"x2":399,"y2":415},"356",[412,420],{"x1":421,"y1":415,"x2":402,"y2":415},"546",[412,423],{"style":424,"x1":425,"y1":426,"x2":425,"y2":406,"stroke":410},"stroke-dasharray:5 5","471","104",[379,428,384,431,384,437,384,441,384,445,384,448,384,451,369],{"style":429,"fill":430},"font-size:14px;text-anchor:middle","var(--text)",[432,433,436],"text",{"x":434,"y":435},"91","68","render(\u003CC \u002F>)",[432,438,440],{"x":434,"y":439},"88","Testing Library",[432,442,444],{"x":443,"y":435},"281","axe(container)",[432,446,447],{"x":443,"y":439},"axe-core rules",[432,449,450],{"x":425,"y":435},"expect →",[432,452,453],{"x":425,"y":439},"toHaveNoViolations",[379,455,384,456,369],{"style":429,"fill":409},[432,457,460],{"x":458,"y":459},"661","76","CI gate",[379,462,384,465,384,470,369],{"style":463,"fill":464},"font-size:13px;text-anchor:middle","var(--muted)",[432,466,469],{"x":467,"y":468},"566","170","layout + color rules",[432,471,473],{"x":467,"y":472},"188","→ defer to Playwright (jsdom has no render)",[475,476],"hr",{},[478,479,481],"h2",{"id":480},"wiring-jest-axe-into-testing-library","Wiring jest-axe into Testing Library",[324,483,484,486,487,490,491,493,494,497],{},[327,485,329],{}," ships an ",[327,488,489],{},"axe"," runner and a custom matcher, ",[327,492,453],{},". The matcher must be registered once via ",[327,495,496],{},"expect.extend"," before any test asserts against it—do this in your global setup file so every spec inherits it.",[499,500,505],"pre",{"className":501,"code":502,"language":503,"meta":504,"style":504},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F test-setup.ts — loaded via setupFilesAfterEach in jest.config\nimport '@testing-library\u002Fjest-dom';\nimport { toHaveNoViolations } from 'jest-axe';\nimport { cleanup } from '@testing-library\u002Freact';\n\n\u002F\u002F Register the matcher globally so every spec can call expect(results).toHaveNoViolations()\nexpect.extend(toHaveNoViolations);\n\n\u002F\u002F Unmount components between tests so axe never inspects leaked DOM from a prior render\nafterEach(() => cleanup());\n","ts","",[327,506,507,515,530,546,561,568,574,587,592,598],{"__ignoreMap":504},[508,509,511],"span",{"class":412,"line":510},1,[508,512,514],{"class":513},"sJ8bj","\u002F\u002F test-setup.ts — loaded via setupFilesAfterEach in jest.config\n",[508,516,518,522,526],{"class":412,"line":517},2,[508,519,521],{"class":520},"szBVR","import",[508,523,525],{"class":524},"sZZnC"," '@testing-library\u002Fjest-dom'",[508,527,529],{"class":528},"sVt8B",";\n",[508,531,533,535,538,541,544],{"class":412,"line":532},3,[508,534,521],{"class":520},[508,536,537],{"class":528}," { toHaveNoViolations } ",[508,539,540],{"class":520},"from",[508,542,543],{"class":524}," 'jest-axe'",[508,545,529],{"class":528},[508,547,549,551,554,556,559],{"class":412,"line":548},4,[508,550,521],{"class":520},[508,552,553],{"class":528}," { cleanup } ",[508,555,540],{"class":520},[508,557,558],{"class":524}," '@testing-library\u002Freact'",[508,560,529],{"class":528},[508,562,564],{"class":412,"line":563},5,[508,565,567],{"emptyLinePlaceholder":566},true,"\n",[508,569,571],{"class":412,"line":570},6,[508,572,573],{"class":513},"\u002F\u002F Register the matcher globally so every spec can call expect(results).toHaveNoViolations()\n",[508,575,577,580,584],{"class":412,"line":576},7,[508,578,579],{"class":528},"expect.",[508,581,583],{"class":582},"sScJk","extend",[508,585,586],{"class":528},"(toHaveNoViolations);\n",[508,588,590],{"class":412,"line":589},8,[508,591,567],{"emptyLinePlaceholder":566},[508,593,595],{"class":412,"line":594},9,[508,596,597],{"class":513},"\u002F\u002F Unmount components between tests so axe never inspects leaked DOM from a prior render\n",[508,599,601,604,607,610,613],{"class":412,"line":600},10,[508,602,603],{"class":582},"afterEach",[508,605,606],{"class":528},"(() ",[508,608,609],{"class":520},"=>",[508,611,612],{"class":582}," cleanup",[508,614,615],{"class":528},"());\n",[499,617,621],{"className":618,"code":619,"language":620,"meta":504,"style":504},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F jest.config.js\nmodule.exports = {\n  testEnvironment: 'jsdom', \u002F\u002F axe-core needs a DOM; jsdom provides one (without layout)\n  setupFilesAfterEach: ['\u003CrootDir>\u002Ftest-setup.ts'],\n};\n","js",[327,622,623,628,646,659,670],{"__ignoreMap":504},[508,624,625],{"class":412,"line":510},[508,626,627],{"class":513},"\u002F\u002F jest.config.js\n",[508,629,630,634,637,640,643],{"class":412,"line":517},[508,631,633],{"class":632},"sj4cs","module",[508,635,636],{"class":528},".",[508,638,639],{"class":632},"exports",[508,641,642],{"class":520}," =",[508,644,645],{"class":528}," {\n",[508,647,648,651,654,656],{"class":412,"line":532},[508,649,650],{"class":528},"  testEnvironment: ",[508,652,653],{"class":524},"'jsdom'",[508,655,340],{"class":528},[508,657,658],{"class":513},"\u002F\u002F axe-core needs a DOM; jsdom provides one (without layout)\n",[508,660,661,664,667],{"class":412,"line":548},[508,662,663],{"class":528},"  setupFilesAfterEach: [",[508,665,666],{"class":524},"'\u003CrootDir>\u002Ftest-setup.ts'",[508,668,669],{"class":528},"],\n",[508,671,672],{"class":412,"line":563},[508,673,674],{"class":528},"};\n",[324,676,677,678,681,682,636],{},"The ",[327,679,680],{},"jsdom"," environment is mandatory: axe-core walks a live DOM tree, so a node-only environment has nothing to inspect. With the matcher registered, every test that renders a component can run axe and assert against the structured result. This is the foundation for the step-by-step walkthrough in ",[332,683,273],{"href":684},"\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe\u002F",[475,686],{},[478,688,690],{"id":689},"rendering-a-component-and-running-axe-against-the-container","Rendering a Component and Running axe Against the Container",[324,692,693,694,697,698,700,701,703,704,707],{},"The core pattern is three lines: render the component, pass the rendered ",[327,695,696],{},"container"," to ",[327,699,489],{},", and assert no violations. Scoping axe to ",[327,702,696],{}," (not ",[327,705,706],{},"document",") keeps the check focused on the component under test rather than test-runner scaffolding.",[499,709,713],{"className":710,"code":711,"language":712,"meta":504,"style":504},"language-tsx shiki shiki-themes github-light github-dark","import { render } from '@testing-library\u002Freact';\nimport { axe } from 'jest-axe';\nimport { PriceCard } from '.\u002FPriceCard';\n\ntest('PriceCard has no axe violations', async () => {\n  const { container } = render(\u003CPriceCard plan=\"pro\" price={49} \u002F>);\n\n  \u002F\u002F axe() is async: it loads rules and walks the DOM, resolving to a results object\n  const results = await axe(container);\n\n  \u002F\u002F The matcher fails with a readable diff listing rule id, impact, and offending nodes\n  expect(results).toHaveNoViolations();\n});\n","tsx",[327,714,715,728,741,755,759,782,829,833,838,856,860,866,880],{"__ignoreMap":504},[508,716,717,719,722,724,726],{"class":412,"line":510},[508,718,521],{"class":520},[508,720,721],{"class":528}," { render } ",[508,723,540],{"class":520},[508,725,558],{"class":524},[508,727,529],{"class":528},[508,729,730,732,735,737,739],{"class":412,"line":517},[508,731,521],{"class":520},[508,733,734],{"class":528}," { axe } ",[508,736,540],{"class":520},[508,738,543],{"class":524},[508,740,529],{"class":528},[508,742,743,745,748,750,753],{"class":412,"line":532},[508,744,521],{"class":520},[508,746,747],{"class":528}," { PriceCard } ",[508,749,540],{"class":520},[508,751,752],{"class":524}," '.\u002FPriceCard'",[508,754,529],{"class":528},[508,756,757],{"class":412,"line":548},[508,758,567],{"emptyLinePlaceholder":566},[508,760,761,764,767,770,772,775,778,780],{"class":412,"line":563},[508,762,763],{"class":582},"test",[508,765,766],{"class":528},"(",[508,768,769],{"class":524},"'PriceCard has no axe violations'",[508,771,340],{"class":528},[508,773,774],{"class":520},"async",[508,776,777],{"class":528}," () ",[508,779,609],{"class":520},[508,781,645],{"class":528},[508,783,784,787,790,792,795,798,801,804,807,810,812,815,818,820,823,826],{"class":412,"line":570},[508,785,786],{"class":520},"  const",[508,788,789],{"class":528}," { ",[508,791,696],{"class":632},[508,793,794],{"class":528}," } ",[508,796,797],{"class":520},"=",[508,799,800],{"class":582}," render",[508,802,803],{"class":528},"(\u003C",[508,805,806],{"class":632},"PriceCard",[508,808,809],{"class":582}," plan",[508,811,797],{"class":520},[508,813,814],{"class":524},"\"pro\"",[508,816,817],{"class":582}," price",[508,819,797],{"class":520},[508,821,822],{"class":528},"{",[508,824,825],{"class":632},"49",[508,827,828],{"class":528},"} \u002F>);\n",[508,830,831],{"class":412,"line":576},[508,832,567],{"emptyLinePlaceholder":566},[508,834,835],{"class":412,"line":589},[508,836,837],{"class":513},"  \u002F\u002F axe() is async: it loads rules and walks the DOM, resolving to a results object\n",[508,839,840,842,845,847,850,853],{"class":412,"line":594},[508,841,786],{"class":520},[508,843,844],{"class":632}," results",[508,846,642],{"class":520},[508,848,849],{"class":520}," await",[508,851,852],{"class":582}," axe",[508,854,855],{"class":528},"(container);\n",[508,857,858],{"class":412,"line":600},[508,859,567],{"emptyLinePlaceholder":566},[508,861,863],{"class":412,"line":862},11,[508,864,865],{"class":513},"  \u002F\u002F The matcher fails with a readable diff listing rule id, impact, and offending nodes\n",[508,867,869,872,875,877],{"class":412,"line":868},12,[508,870,871],{"class":582},"  expect",[508,873,874],{"class":528},"(results).",[508,876,453],{"class":582},[508,878,879],{"class":528},"();\n",[508,881,883],{"class":412,"line":882},13,[508,884,885],{"class":528},"});\n",[324,887,888,889,892,893,896,897,900,901,904,905,908,909,911,912,914,915,917,918,921,922,925],{},"This catches the rules axe can evaluate without layout: a ",[327,890,891],{},"\u003Cbutton>"," with no accessible name, an ",[327,894,895],{},"\u003Cimg>"," missing ",[327,898,899],{},"alt",", an ",[327,902,903],{},"aria-labelledby"," pointing at a non-existent id, a list item outside a list, duplicate ",[327,906,907],{},"id"," attributes feeding ",[327,910,354],{}," references. These map directly to ",[327,913,343],{}," and ",[327,916,339],{},". Because ",[327,919,920],{},"axe()"," is asynchronous, always ",[327,923,924],{},"await"," it—an unawaited promise produces a passing test that checked nothing.",[324,927,928,929,933,934,936,937,939],{},"One caveat: a component that renders to a ",[332,930,932],{"href":931},"\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci\u002F","React portal"," lands outside ",[327,935,696],{},", so scoping to ",[327,938,696],{}," will silently skip it. That scope decision is the single most common source of false-green tests, covered in depth in the CI debugging guide below.",[475,941],{},[478,943,945],{"id":944},"scoping-and-disabling-specific-rules-per-test","Scoping and Disabling Specific Rules Per Test",[324,947,948,949,636],{},"Axe-core runs its full ruleset by default. Sometimes you need to narrow it—either to assert one specific rule in isolation, or to suppress a rule that cannot run meaningfully in jsdom. Pass a configuration object as the second argument to ",[327,950,489],{},[499,952,954],{"className":710,"code":953,"language":712,"meta":504,"style":504},"test('Avatar disables the contrast rule it cannot evaluate in jsdom', async () => {\n  const { container } = render(\u003CAvatar name=\"Ada Lovelace\" \u002F>);\n\n  const results = await axe(container, {\n    rules: {\n      \u002F\u002F color-contrast needs computed layout\u002Fpaint, which jsdom lacks—turn it off\n      \u002F\u002F rather than let it false-negative. Real contrast lives in Playwright\u002FCI.\n      'color-contrast': { enabled: false },\n    },\n  });\n\n  expect(results).toHaveNoViolations();\n});\n",[327,955,956,975,1005,1009,1024,1029,1034,1039,1053,1058,1063,1067,1077],{"__ignoreMap":504},[508,957,958,960,962,965,967,969,971,973],{"class":412,"line":510},[508,959,763],{"class":582},[508,961,766],{"class":528},[508,963,964],{"class":524},"'Avatar disables the contrast rule it cannot evaluate in jsdom'",[508,966,340],{"class":528},[508,968,774],{"class":520},[508,970,777],{"class":528},[508,972,609],{"class":520},[508,974,645],{"class":528},[508,976,977,979,981,983,985,987,989,991,994,997,999,1002],{"class":412,"line":517},[508,978,786],{"class":520},[508,980,789],{"class":528},[508,982,696],{"class":632},[508,984,794],{"class":528},[508,986,797],{"class":520},[508,988,800],{"class":582},[508,990,803],{"class":528},[508,992,993],{"class":632},"Avatar",[508,995,996],{"class":582}," name",[508,998,797],{"class":520},[508,1000,1001],{"class":524},"\"Ada Lovelace\"",[508,1003,1004],{"class":528}," \u002F>);\n",[508,1006,1007],{"class":412,"line":532},[508,1008,567],{"emptyLinePlaceholder":566},[508,1010,1011,1013,1015,1017,1019,1021],{"class":412,"line":548},[508,1012,786],{"class":520},[508,1014,844],{"class":632},[508,1016,642],{"class":520},[508,1018,849],{"class":520},[508,1020,852],{"class":582},[508,1022,1023],{"class":528},"(container, {\n",[508,1025,1026],{"class":412,"line":563},[508,1027,1028],{"class":528},"    rules: {\n",[508,1030,1031],{"class":412,"line":570},[508,1032,1033],{"class":513},"      \u002F\u002F color-contrast needs computed layout\u002Fpaint, which jsdom lacks—turn it off\n",[508,1035,1036],{"class":412,"line":576},[508,1037,1038],{"class":513},"      \u002F\u002F rather than let it false-negative. Real contrast lives in Playwright\u002FCI.\n",[508,1040,1041,1044,1047,1050],{"class":412,"line":589},[508,1042,1043],{"class":524},"      'color-contrast'",[508,1045,1046],{"class":528},": { enabled: ",[508,1048,1049],{"class":632},"false",[508,1051,1052],{"class":528}," },\n",[508,1054,1055],{"class":412,"line":594},[508,1056,1057],{"class":528},"    },\n",[508,1059,1060],{"class":412,"line":600},[508,1061,1062],{"class":528},"  });\n",[508,1064,1065],{"class":412,"line":862},[508,1066,567],{"emptyLinePlaceholder":566},[508,1068,1069,1071,1073,1075],{"class":412,"line":868},[508,1070,871],{"class":582},[508,1072,874],{"class":528},[508,1074,453],{"class":582},[508,1076,879],{"class":528},[508,1078,1079],{"class":412,"line":882},[508,1080,885],{"class":528},[324,1082,1083],{},"You can also restrict the run to specific tags or a single rule when writing a targeted regression test:",[499,1085,1087],{"className":710,"code":1086,"language":712,"meta":504,"style":504},"const results = await axe(container, {\n  runOnly: { type: 'rule', values: ['button-name'] }, \u002F\u002F assert only that buttons are named\n});\n",[327,1088,1089,1104,1124],{"__ignoreMap":504},[508,1090,1091,1094,1096,1098,1100,1102],{"class":412,"line":510},[508,1092,1093],{"class":520},"const",[508,1095,844],{"class":632},[508,1097,642],{"class":520},[508,1099,849],{"class":520},[508,1101,852],{"class":582},[508,1103,1023],{"class":528},[508,1105,1106,1109,1112,1115,1118,1121],{"class":412,"line":517},[508,1107,1108],{"class":528},"  runOnly: { type: ",[508,1110,1111],{"class":524},"'rule'",[508,1113,1114],{"class":528},", values: [",[508,1116,1117],{"class":524},"'button-name'",[508,1119,1120],{"class":528},"] }, ",[508,1122,1123],{"class":513},"\u002F\u002F assert only that buttons are named\n",[508,1125,1126],{"class":412,"line":532},[508,1127,885],{"class":528},[324,1129,1130,1131,636],{},"Disable rules deliberately and document why. A blanket disable hides real defects; a scoped, commented disable communicates that the rule belongs to a different test layer. Resist suppressing a rule just to make a test pass—that converts an accessibility gate into decorative noise. For project-wide axe configuration shared across component and end-to-end suites, see ",[332,1132,243],{"href":1133},"\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002F",[475,1135],{},[478,1137,1139],{"id":1138},"testing-dynamic-component-states","Testing Dynamic Component States",[324,1141,1142],{},"A component's initial render is rarely where accessibility breaks. Regressions appear when a modal opens, a form surfaces validation errors, or a menu expands. Each of these is a distinct DOM state, and each needs its own axe pass. Drive the component into the state with Testing Library, wait for it to settle, then run axe.",[499,1144,1146],{"className":710,"code":1145,"language":712,"meta":504,"style":504},"import { render, screen } from '@testing-library\u002Freact';\nimport userEvent from '@testing-library\u002Fuser-event';\nimport { axe } from 'jest-axe';\nimport { SettingsDialog } from '.\u002FSettingsDialog';\n\ntest('open dialog has no violations and a proper dialog contract', async () => {\n  const user = userEvent.setup();\n  const { container } = render(\u003CSettingsDialog \u002F>);\n\n  \u002F\u002F Drive the component into its OPEN state—the initial render never showed the dialog\n  await user.click(screen.getByRole('button', { name: \u002Fopen settings\u002Fi }));\n\n  \u002F\u002F findByRole waits for the dialog to mount before axe inspects it\n  const dialog = await screen.findByRole('dialog');\n\n  \u002F\u002F Run axe on the now-open DOM; this catches a focus-trap container with a missing name\n  const results = await axe(container);\n  expect(results).toHaveNoViolations();\n\n  \u002F\u002F aria-modal is a contract axe will not enforce for you; assert it explicitly\n  expect(dialog).toHaveAttribute('aria-modal', 'true');\n});\n",[327,1147,1148,1161,1175,1187,1201,1205,1224,1241,1262,1266,1271,1311,1315,1320,1346,1351,1357,1372,1383,1388,1394,1417],{"__ignoreMap":504},[508,1149,1150,1152,1155,1157,1159],{"class":412,"line":510},[508,1151,521],{"class":520},[508,1153,1154],{"class":528}," { render, screen } ",[508,1156,540],{"class":520},[508,1158,558],{"class":524},[508,1160,529],{"class":528},[508,1162,1163,1165,1168,1170,1173],{"class":412,"line":517},[508,1164,521],{"class":520},[508,1166,1167],{"class":528}," userEvent ",[508,1169,540],{"class":520},[508,1171,1172],{"class":524}," '@testing-library\u002Fuser-event'",[508,1174,529],{"class":528},[508,1176,1177,1179,1181,1183,1185],{"class":412,"line":532},[508,1178,521],{"class":520},[508,1180,734],{"class":528},[508,1182,540],{"class":520},[508,1184,543],{"class":524},[508,1186,529],{"class":528},[508,1188,1189,1191,1194,1196,1199],{"class":412,"line":548},[508,1190,521],{"class":520},[508,1192,1193],{"class":528}," { SettingsDialog } ",[508,1195,540],{"class":520},[508,1197,1198],{"class":524}," '.\u002FSettingsDialog'",[508,1200,529],{"class":528},[508,1202,1203],{"class":412,"line":563},[508,1204,567],{"emptyLinePlaceholder":566},[508,1206,1207,1209,1211,1214,1216,1218,1220,1222],{"class":412,"line":570},[508,1208,763],{"class":582},[508,1210,766],{"class":528},[508,1212,1213],{"class":524},"'open dialog has no violations and a proper dialog contract'",[508,1215,340],{"class":528},[508,1217,774],{"class":520},[508,1219,777],{"class":528},[508,1221,609],{"class":520},[508,1223,645],{"class":528},[508,1225,1226,1228,1231,1233,1236,1239],{"class":412,"line":576},[508,1227,786],{"class":520},[508,1229,1230],{"class":632}," user",[508,1232,642],{"class":520},[508,1234,1235],{"class":528}," userEvent.",[508,1237,1238],{"class":582},"setup",[508,1240,879],{"class":528},[508,1242,1243,1245,1247,1249,1251,1253,1255,1257,1260],{"class":412,"line":589},[508,1244,786],{"class":520},[508,1246,789],{"class":528},[508,1248,696],{"class":632},[508,1250,794],{"class":528},[508,1252,797],{"class":520},[508,1254,800],{"class":582},[508,1256,803],{"class":528},[508,1258,1259],{"class":632},"SettingsDialog",[508,1261,1004],{"class":528},[508,1263,1264],{"class":412,"line":594},[508,1265,567],{"emptyLinePlaceholder":566},[508,1267,1268],{"class":412,"line":600},[508,1269,1270],{"class":513},"  \u002F\u002F Drive the component into its OPEN state—the initial render never showed the dialog\n",[508,1272,1273,1276,1279,1282,1285,1288,1290,1293,1296,1299,1303,1305,1308],{"class":412,"line":862},[508,1274,1275],{"class":520},"  await",[508,1277,1278],{"class":528}," user.",[508,1280,1281],{"class":582},"click",[508,1283,1284],{"class":528},"(screen.",[508,1286,1287],{"class":582},"getByRole",[508,1289,766],{"class":528},[508,1291,1292],{"class":524},"'button'",[508,1294,1295],{"class":528},", { name:",[508,1297,1298],{"class":524}," \u002F",[508,1300,1302],{"class":1301},"sA_wV","open settings",[508,1304,86],{"class":524},[508,1306,1307],{"class":520},"i",[508,1309,1310],{"class":528}," }));\n",[508,1312,1313],{"class":412,"line":868},[508,1314,567],{"emptyLinePlaceholder":566},[508,1316,1317],{"class":412,"line":882},[508,1318,1319],{"class":513},"  \u002F\u002F findByRole waits for the dialog to mount before axe inspects it\n",[508,1321,1323,1325,1328,1330,1332,1335,1338,1340,1343],{"class":412,"line":1322},14,[508,1324,786],{"class":520},[508,1326,1327],{"class":632}," dialog",[508,1329,642],{"class":520},[508,1331,849],{"class":520},[508,1333,1334],{"class":528}," screen.",[508,1336,1337],{"class":582},"findByRole",[508,1339,766],{"class":528},[508,1341,1342],{"class":524},"'dialog'",[508,1344,1345],{"class":528},");\n",[508,1347,1349],{"class":412,"line":1348},15,[508,1350,567],{"emptyLinePlaceholder":566},[508,1352,1354],{"class":412,"line":1353},16,[508,1355,1356],{"class":513},"  \u002F\u002F Run axe on the now-open DOM; this catches a focus-trap container with a missing name\n",[508,1358,1360,1362,1364,1366,1368,1370],{"class":412,"line":1359},17,[508,1361,786],{"class":520},[508,1363,844],{"class":632},[508,1365,642],{"class":520},[508,1367,849],{"class":520},[508,1369,852],{"class":582},[508,1371,855],{"class":528},[508,1373,1375,1377,1379,1381],{"class":412,"line":1374},18,[508,1376,871],{"class":582},[508,1378,874],{"class":528},[508,1380,453],{"class":582},[508,1382,879],{"class":528},[508,1384,1386],{"class":412,"line":1385},19,[508,1387,567],{"emptyLinePlaceholder":566},[508,1389,1391],{"class":412,"line":1390},20,[508,1392,1393],{"class":513},"  \u002F\u002F aria-modal is a contract axe will not enforce for you; assert it explicitly\n",[508,1395,1397,1399,1402,1405,1407,1410,1412,1415],{"class":412,"line":1396},21,[508,1398,871],{"class":582},[508,1400,1401],{"class":528},"(dialog).",[508,1403,1404],{"class":582},"toHaveAttribute",[508,1406,766],{"class":528},[508,1408,1409],{"class":524},"'aria-modal'",[508,1411,340],{"class":528},[508,1413,1414],{"class":524},"'true'",[508,1416,1345],{"class":528},[508,1418,1420],{"class":412,"line":1419},22,[508,1421,885],{"class":528},[324,1423,1424,1425,1428,1429,1432,1433,1436,1437,1440,1441,1444],{},"The same discipline applies to a form in its error state: submit invalid data, wait for the error region, then run axe to confirm each invalid field carries ",[327,1426,1427],{},"aria-invalid"," and an ",[327,1430,1431],{},"aria-describedby"," that resolves to the visible error text—the structural side of ",[327,1434,1435],{},"3.3.1 Error Identification",". An expanded disclosure menu needs ",[327,1438,1439],{},"aria-expanded=\"true\""," and a ",[327,1442,1443],{},"aria-controls"," target present in the DOM. Run axe once per meaningful state; a single render-time pass leaves the most fragile transitions untested.",[475,1446],{},[478,1448,1450],{"id":1449},"combining-axe-with-role-and-name-queries","Combining axe With Role and Name Queries",[324,1452,1453,1454,1458,1459,1461,1462,1464,1465,1468,1469,1471],{},"Axe-core verifies that the DOM is structurally valid—but it does not know your intent. It cannot tell you that the dialog ",[1455,1456,1457],"em",{},"should"," be labeled \"Edit profile,\" that the error message ",[1455,1460,1457],{}," be associated with the email field, or that the active tab ",[1455,1463,1457],{}," be ",[327,1466,1467],{},"Account",". Those are application contracts, and you assert them with Testing Library's ",[327,1470,1287],{}," and accessible-name queries alongside the axe pass.",[499,1473,1475],{"className":710,"code":1474,"language":712,"meta":504,"style":504},"test('error message is associated with the field it describes', async () => {\n  const user = userEvent.setup();\n  const { container } = render(\u003CEmailForm \u002F>);\n\n  await user.click(screen.getByRole('button', { name: \u002Fsave\u002Fi }));\n\n  \u002F\u002F Testing Library resolves the field by its accessible name—the same name AT exposes\n  const email = await screen.findByRole('textbox', { name: \u002Femail\u002Fi });\n\n  \u002F\u002F axe confirms the wiring is structurally valid (id targets resolve, role is correct)\n  expect(await axe(container)).toHaveNoViolations();\n\n  \u002F\u002F Then assert the semantic contract axe cannot: invalid state + the RIGHT description\n  expect(email).toHaveAttribute('aria-invalid', 'true');\n  expect(email).toHaveAccessibleDescription(\u002Fenter a valid email\u002Fi);\n});\n",[327,1476,1477,1496,1510,1531,1535,1564,1568,1573,1607,1611,1616,1633,1637,1642,1662,1684],{"__ignoreMap":504},[508,1478,1479,1481,1483,1486,1488,1490,1492,1494],{"class":412,"line":510},[508,1480,763],{"class":582},[508,1482,766],{"class":528},[508,1484,1485],{"class":524},"'error message is associated with the field it describes'",[508,1487,340],{"class":528},[508,1489,774],{"class":520},[508,1491,777],{"class":528},[508,1493,609],{"class":520},[508,1495,645],{"class":528},[508,1497,1498,1500,1502,1504,1506,1508],{"class":412,"line":517},[508,1499,786],{"class":520},[508,1501,1230],{"class":632},[508,1503,642],{"class":520},[508,1505,1235],{"class":528},[508,1507,1238],{"class":582},[508,1509,879],{"class":528},[508,1511,1512,1514,1516,1518,1520,1522,1524,1526,1529],{"class":412,"line":532},[508,1513,786],{"class":520},[508,1515,789],{"class":528},[508,1517,696],{"class":632},[508,1519,794],{"class":528},[508,1521,797],{"class":520},[508,1523,800],{"class":582},[508,1525,803],{"class":528},[508,1527,1528],{"class":632},"EmailForm",[508,1530,1004],{"class":528},[508,1532,1533],{"class":412,"line":548},[508,1534,567],{"emptyLinePlaceholder":566},[508,1536,1537,1539,1541,1543,1545,1547,1549,1551,1553,1555,1558,1560,1562],{"class":412,"line":563},[508,1538,1275],{"class":520},[508,1540,1278],{"class":528},[508,1542,1281],{"class":582},[508,1544,1284],{"class":528},[508,1546,1287],{"class":582},[508,1548,766],{"class":528},[508,1550,1292],{"class":524},[508,1552,1295],{"class":528},[508,1554,1298],{"class":524},[508,1556,1557],{"class":1301},"save",[508,1559,86],{"class":524},[508,1561,1307],{"class":520},[508,1563,1310],{"class":528},[508,1565,1566],{"class":412,"line":570},[508,1567,567],{"emptyLinePlaceholder":566},[508,1569,1570],{"class":412,"line":576},[508,1571,1572],{"class":513},"  \u002F\u002F Testing Library resolves the field by its accessible name—the same name AT exposes\n",[508,1574,1575,1577,1580,1582,1584,1586,1588,1590,1593,1595,1597,1600,1602,1604],{"class":412,"line":589},[508,1576,786],{"class":520},[508,1578,1579],{"class":632}," email",[508,1581,642],{"class":520},[508,1583,849],{"class":520},[508,1585,1334],{"class":528},[508,1587,1337],{"class":582},[508,1589,766],{"class":528},[508,1591,1592],{"class":524},"'textbox'",[508,1594,1295],{"class":528},[508,1596,1298],{"class":524},[508,1598,1599],{"class":1301},"email",[508,1601,86],{"class":524},[508,1603,1307],{"class":520},[508,1605,1606],{"class":528}," });\n",[508,1608,1609],{"class":412,"line":594},[508,1610,567],{"emptyLinePlaceholder":566},[508,1612,1613],{"class":412,"line":600},[508,1614,1615],{"class":513},"  \u002F\u002F axe confirms the wiring is structurally valid (id targets resolve, role is correct)\n",[508,1617,1618,1620,1622,1624,1626,1629,1631],{"class":412,"line":862},[508,1619,871],{"class":582},[508,1621,766],{"class":528},[508,1623,924],{"class":520},[508,1625,852],{"class":582},[508,1627,1628],{"class":528},"(container)).",[508,1630,453],{"class":582},[508,1632,879],{"class":528},[508,1634,1635],{"class":412,"line":868},[508,1636,567],{"emptyLinePlaceholder":566},[508,1638,1639],{"class":412,"line":882},[508,1640,1641],{"class":513},"  \u002F\u002F Then assert the semantic contract axe cannot: invalid state + the RIGHT description\n",[508,1643,1644,1646,1649,1651,1653,1656,1658,1660],{"class":412,"line":1322},[508,1645,871],{"class":582},[508,1647,1648],{"class":528},"(email).",[508,1650,1404],{"class":582},[508,1652,766],{"class":528},[508,1654,1655],{"class":524},"'aria-invalid'",[508,1657,340],{"class":528},[508,1659,1414],{"class":524},[508,1661,1345],{"class":528},[508,1663,1664,1666,1668,1671,1673,1675,1678,1680,1682],{"class":412,"line":1348},[508,1665,871],{"class":582},[508,1667,1648],{"class":528},[508,1669,1670],{"class":582},"toHaveAccessibleDescription",[508,1672,766],{"class":528},[508,1674,86],{"class":524},[508,1676,1677],{"class":1301},"enter a valid email",[508,1679,86],{"class":524},[508,1681,1307],{"class":520},[508,1683,1345],{"class":528},[508,1685,1686],{"class":412,"line":1353},[508,1687,885],{"class":528},[324,1689,1690,1691,1693,1694,1697,1698,1700],{},"The division is clean: axe owns \"is this structurally legal,\" ",[327,1692,1287],{},"\u002Fname queries own \"does it mean the right thing.\" Querying by role and accessible name also forces your tests to interact with the component the way assistive technology does, which surfaces naming regressions that a ",[327,1695,1696],{},"data-testid"," selector would sail past—reinforcing ",[327,1699,343],{}," at the assertion level.",[475,1702],{},[478,1704,1706],{"id":1705},"jsdom-limitations-and-what-to-push-to-playwright","jsdom Limitations and What to Push to Playwright",[324,1708,1709],{},"jsdom implements the DOM API but has no layout or paint pipeline. Element geometry is effectively zero, computed styles are incomplete, and nothing is ever truly \"visible\" in a rendered sense. Several axe-core rules depend on exactly that information and therefore cannot produce trustworthy results in jest-axe:",[1711,1712,1713,1726,1732,1744],"ul",{},[1714,1715,1716,1722,1723,636],"li",{},[1717,1718,1719],"strong",{},[327,1720,1721],{},"color-contrast"," — needs computed foreground\u002Fbackground colors and font metrics; runs as a no-op or false result in jsdom. Owns ",[327,1724,1725],{},"1.4.3 Contrast (Minimum)",[1714,1727,1728,1731],{},[1717,1729,1730],{},"Visibility-dependent rules"," — anything that checks whether an element is on-screen, occluded, or sized below a target threshold has no geometry to read.",[1714,1733,1734,1739,1740,1743],{},[1717,1735,1736],{},[327,1737,1738],{},"target-size"," (",[327,1741,1742],{},"2.5.8 Target Size (Minimum)",") — requires real pixel dimensions jsdom never computes.",[1714,1745,1746,1739,1749,1752],{},[1717,1747,1748],{},"Reflow and zoom behavior",[327,1750,1751],{},"1.4.10 Reflow",") — inherently a rendered-viewport concern.",[324,1754,1755,1756,1758],{},"Disable these in your jest-axe config so they cannot false-negative, and assert them in a real browser instead. ",[332,1757,279],{"href":358}," runs the same axe-core engine against Chromium, where layout and color exist, making it the correct home for contrast, target size, and focus-visibility checks. Use component tests for fast, structural feedback on every state; use end-to-end tests for the rendered truth.",[324,1760,1761,1764,1765,1768,1769,1771,1772,1774],{},[1717,1762,1763],{},"How to verify:"," Run ",[327,1766,1767],{},"npx jest --runInBand"," locally and confirm the suite fails loudly when you delete a label or break an ",[327,1770,1431],{}," reference—if it stays green, your scope or ",[327,1773,924],{}," is wrong. Then run an end-to-end axe pass for the layout\u002Fcolor rules jsdom skipped, and finish with a manual sweep in NVDA or VoiceOver to confirm the announced names and states match your role\u002Fname assertions.",[475,1776],{},[478,1778,1780],{"id":1779},"key-takeaways","Key Takeaways",[1711,1782,1783,1795,1811,1820,1823,1829],{},[1714,1784,1785,1786,1788,1789,1791,1792,1794],{},"Register ",[327,1787,453],{}," once via ",[327,1790,496],{}," in global setup; require the ",[327,1793,680],{}," environment.",[1714,1796,1797,1798,1801,1802,1801,1805,1808,1809,636],{},"The base pattern is ",[327,1799,1800],{},"render"," → ",[327,1803,1804],{},"await axe(container)",[327,1806,1807],{},"expect(results).toHaveNoViolations()","; always ",[327,1810,924],{},[1714,1812,1813,1814,1816,1817,1819],{},"Scope axe to the rendered ",[327,1815,696],{},", but remember portals escape it—scope to ",[327,1818,706],{}," when testing portal content.",[1714,1821,1822],{},"Test every meaningful dynamic state (open modal, error form, expanded menu) with its own axe pass.",[1714,1824,1825,1826,1828],{},"Pair axe with ",[327,1827,1287],{},"\u002Faccessible-name queries to assert the semantic contracts axe cannot check.",[1714,1830,1831],{},"Disable layout- and color-dependent rules in jsdom and push them to Playwright.",[475,1833],{},[478,1835,1837],{"id":1836},"frequently-asked-questions","Frequently Asked Questions",[324,1839,1840,1843,1844,1846,1847,1849,1850,1853,1854,1857,1858,86,1861,1864],{},[1717,1841,1842],{},"Why does my jest-axe test pass even though the component is clearly inaccessible?","\nThe most common causes are an unawaited ",[327,1845,920],{}," call (the promise never resolves before the assertion), scoping to ",[327,1848,696],{}," when the content renders into a portal on ",[327,1851,1852],{},"document.body",", or running axe before the dynamic state has mounted. Confirm you ",[327,1855,1856],{},"await axe(...)",", choose the right scope, and use ",[327,1859,1860],{},"findBy",[327,1862,1863],{},"waitFor"," to let async DOM settle first.",[324,1866,1867,1870,1871,1873,1874,636],{},[1717,1868,1869],{},"Can jest-axe catch color-contrast problems?","\nNo—jsdom has no layout or rendering, so the ",[327,1872,1721],{}," rule cannot compute foreground and background colors and either no-ops or returns an unreliable result. Disable it in your jest-axe config and verify contrast in a real browser via ",[332,1875,1876],{"href":358},"Playwright",[324,1878,1879,1882,1883,1885,1886,1888],{},[1717,1880,1881],{},"Should I run axe on the whole document or just the component's container?","\nDefault to the rendered ",[327,1884,696],{}," so the check stays focused on the component under test. Switch to ",[327,1887,706],{}," only when the component renders into a portal (modals, tooltips, toasts) that mounts outside that container—otherwise axe silently skips the portal content.",[324,1890,1891,1894,1895,1898,1899,1901],{},[1717,1892,1893],{},"Does passing jest-axe mean my component is fully accessible?","\nNo. Axe-core catches a machine-checkable subset of WCAG—missing names, broken references, invalid roles. It cannot judge whether a label is ",[1455,1896,1897],{},"correct",", whether focus moves sensibly, or whether contrast passes in jsdom. Pair axe with ",[327,1900,1287],{},"\u002Fname assertions, end-to-end tests, and manual screen-reader checks.",[324,1903,1904,1907,1908,1910],{},[1717,1905,1906],{},"Where should I disable an axe rule—per test or globally?","\nDisable per test when the rule is irrelevant to that specific component, and globally (in shared config) only for rules that can never run meaningfully in jsdom, like ",[327,1909,1721],{},". Always add a comment explaining why, so a suppressed rule reads as a deliberate layer decision rather than a hidden defect.",[475,1912],{},[478,1914,1916],{"id":1915},"related-guides","Related guides",[1711,1918,1919,1923,1927,1931,1936,1940],{},[1714,1920,1921],{},[332,1922,335],{"href":334},[1714,1924,1925],{},[332,1926,243],{"href":1133},[1714,1928,1929],{},[332,1930,279],{"href":358},[1714,1932,1933],{},[332,1934,297],{"href":1935},"\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002F",[1714,1937,1938],{},[332,1939,273],{"href":684},[1714,1941,1942],{},[332,1943,267],{"href":931},[1945,1946,1947],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":504,"searchDepth":517,"depth":517,"links":1949},[1950,1951,1952,1953,1954,1955,1956,1957,1958],{"id":480,"depth":517,"text":481},{"id":689,"depth":517,"text":690},{"id":944,"depth":517,"text":945},{"id":1138,"depth":517,"text":1139},{"id":1449,"depth":517,"text":1450},{"id":1705,"depth":517,"text":1706},{"id":1779,"depth":517,"text":1780},{"id":1836,"depth":517,"text":1837},{"id":1915,"depth":517,"text":1916},null,"Catch accessibility regressions in React components before they ship—wiring jest-axe into Testing Library, asserting no violations, and testing dynamic component states.","md",{},false,{"title":261,"description":1960},"h99U1_BnZrmHj2Ipa6RoPWdU54Y-Ko9J3midQL7guBI",[1967,2006,2007,2070],{"title":5,"path":6,"stem":7,"children":1968},[1969,1970,1973,1976,1982,1988,1997,2003],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":1971},[1972],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":1974},[1975],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":1977},[1978,1979],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":1980},[1981],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":1983},[1984,1985],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":1986},[1987],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":1989},[1990,1991,1994],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":1992},[1993],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":1995},[1996],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":1998},[1999,2000],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2001},[2002],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2004},[2005],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2008},[2009,2010,2016,2028,2040,2043,2052,2064],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2011},[2012,2013],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2014},[2015],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2017},[2018,2019,2022,2025],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2020},[2021],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2023},[2024],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2026},[2027],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2029},[2030,2031,2034,2037],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2032},[2033],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2035},[2036],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2038},[2039],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2041},[2042],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2044},[2045,2046,2049],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2047},[2048],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2050},[2051],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2053},[2054,2055,2058,2061],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2056},[2057],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2059},[2060],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2062},[2063],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2065},[2066,2067],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2068},[2069],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2071},[2072,2073,2082,2091,2100,2109],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2074},[2075,2076,2079],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2077},[2078],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2080},[2081],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2083},[2084,2085,2088],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2086},[2087],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2089},[2090],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2092},[2093,2094,2097],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2095},[2096],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2098},[2099],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2101},[2102,2103,2106],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2104},[2105],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2107},[2108],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2110},[2111,2112,2115],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2113},[2114],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2116},[2117],{"title":309,"path":310,"stem":311},1781785523930]