[{"data":1,"prerenderedAt":2153},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules\u002F":314,"content-navigation":2001},[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":255,"body":316,"date":1994,"description":1995,"extension":1996,"image":1994,"meta":1997,"modifiedAt":1994,"navigation":546,"noindex":1998,"path":256,"publishedAt":1994,"seo":1999,"stem":257,"updatedAt":1994,"__hash__":2000},"content\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules\u002Findex.md",{"type":317,"value":318,"toc":1981},"minimark",[319,323,354,359,398,401,405,411,457,474,476,480,502,983,990,1077,1090,1092,1096,1105,1245,1251,1253,1257,1260,1663,1680,1688,1690,1694,1738,1740,1744,1747,1804,1807,1809,1813,1870,1872,1876,1881,1883,1887,1910,1922,1934,1953,1955,1959,1977],[320,321,255],"h1",{"id":322},"writing-custom-axe-core-rules",[324,325,326,327,331,332,335,336,339,340,344,345,348,349,353],"p",{},"axe-core's built-in rules cover the machine-checkable parts of WCAG, but they can't know your design system's conventions: that every icon-only button must carry a specific data attribute, that your ",[328,329,330],"code",{},"Modal"," component must always render an ",[328,333,334],{},"aria-labelledby",", or that a custom widget must expose a particular role. ",[328,337,338],{},"axe.configure()"," lets you register your own ",[341,342,343],"strong",{},"checks"," and ",[341,346,347],{},"rules"," so these project-specific requirements run through the same engine—and the same violations\u002Fincomplete\u002Fpasses pipeline—as the standard ones. This guide, part of ",[350,351,243],"a",{"href":352},"\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002F",", shows how to author, scope, and test a custom rule, and when a lint rule or jest-axe assertion is the better tool.",[355,356,358],"h2",{"id":357},"prerequisites","Prerequisites",[360,361,362,370,377],"ul",{},[363,364,365,366,369],"li",{},"A working axe-core integration (browser, ",[328,367,368],{},"@axe-core\u002Freact",", jest-axe, or Playwright).",[363,371,372,373,376],{},"A concrete, falsifiable requirement—something a small ",[328,374,375],{},"evaluate"," function can decide true\u002Ffalse from the DOM.",[363,378,379,380,382,383,386,387,386,390,393,394,397],{},"Familiarity with the result model: ",[328,381,338],{}," rules emit the same ",[328,384,385],{},"violations"," \u002F ",[328,388,389],{},"incomplete",[328,391,392],{},"passes"," buckets covered in the ",[350,395,396],{"href":352},"guide",".",[399,400],"hr",{},[355,402,404],{"id":403},"anatomy-checks-vs-rules","Anatomy: Checks vs Rules",[324,406,407,408,410],{},"axe separates two concepts, and ",[328,409,338],{}," registers both:",[360,412,413,435],{},[363,414,415,416,419,420,422,423,426,427,430,431,434],{},"A ",[341,417,418],{},"check"," is the unit of logic. Its ",[328,421,375],{}," function receives a DOM node and returns ",[328,424,425],{},"true"," (passed), ",[328,428,429],{},"false"," (failed), or ",[328,432,433],{},"undefined"," (incomplete—needs review). This is where your actual test lives.",[363,436,415,437,440,441,444,445,448,449,452,453,456],{},[341,438,439],{},"rule"," binds one or more checks to a set of nodes via a CSS ",[328,442,443],{},"selector"," (and optional ",[328,446,447],{},"matches"," predicate), and carries metadata: ",[328,450,451],{},"id",", ",[328,454,455],{},"tags",", and the help text shown in reports.",[324,458,459,460,86,463,466,467,470,471,473],{},"A rule passes a node only if its checks resolve favourably; a single failing ",[328,461,462],{},"any",[328,464,465],{},"all"," check produces a violation. Mapping that to ",[328,468,469],{},"4.1.2 Name, Role, Value",": a check that returns ",[328,472,429],{}," when an interactive element lacks a computed accessible name behaves exactly like axe's own name checks.",[399,475],{},[355,477,479],{"id":478},"registering-a-custom-check-and-rule","Registering a Custom Check and Rule",[324,481,482,483,486,487,452,490,492,493,496,497,501],{},"Below is a complete, project-specific rule: every icon-only button in our design system is marked ",[328,484,485],{},"data-icon-button",", and must expose an accessible name (via ",[328,488,489],{},"aria-label",[328,491,334],{},", or visually-hidden text). Built-in ",[328,494,495],{},"button-name"," covers generic buttons, but we want a dedicated rule with our own help text and tag so violations are obviously ",[498,499,500],"em",{},"ours"," in reports.",[503,504,509],"pre",{"className":505,"code":506,"language":507,"meta":508,"style":508},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F axe-custom-rules.ts\nimport axe from 'axe-core';\n\naxe.configure({\n  \u002F\u002F 1) The check: the actual decision logic.\n  checks: [\n    {\n      id: 'icon-button-has-name',\n      \u002F\u002F evaluate runs in the page context against each matched node.\n      evaluate(node: HTMLElement) {\n        \u002F\u002F axe.commons exposes the same accessible-name computation\n        \u002F\u002F the engine uses for 4.1.2 Name, Role, Value.\n        const name = (axe as any).commons.text.accessibleText(node);\n        return typeof name === 'string' && name.trim().length > 0;\n      },\n      \u002F\u002F Message shown for pass\u002Ffail in the result data.\n      metadata: {\n        impact: 'serious',\n        messages: {\n          pass: 'Icon button exposes an accessible name.',\n          fail: 'Icon-only button has no accessible name (aria-label, aria-labelledby, or visually-hidden text).',\n        },\n      },\n    },\n  ],\n\n  \u002F\u002F 2) The rule: bind the check to the nodes it should run on.\n  rules: [\n    {\n      id: 'ds-icon-button-name',\n      \u002F\u002F Only our design-system icon buttons match.\n      selector: 'button[data-icon-button], [role=\"button\"][data-icon-button]',\n      \u002F\u002F Optional secondary predicate for finer matching.\n      matches: (node: HTMLElement) => node.querySelector('svg') !== null,\n      \u002F\u002F Tag it so teams can include\u002Fexclude it via runOnly.\n      tags: ['cat.name-role-value', 'best-practice', 'ds-internal'],\n      \u002F\u002F 'any' = passes if any listed check passes.\n      any: ['icon-button-has-name'],\n      metadata: {\n        description: 'Design-system icon buttons must have an accessible name.',\n        help: 'Add aria-label or visually-hidden text to icon-only buttons.',\n        helpUrl: 'https:\u002F\u002Finternal.docs\u002Fa11y\u002Ficon-button-name',\n      },\n    },\n  ],\n});\n","ts","",[328,510,511,520,541,548,561,567,573,579,591,597,619,625,631,662,703,709,715,721,732,738,749,760,766,771,777,783,788,794,800,805,815,821,832,838,880,886,908,914,924,929,940,951,962,967,972,977],{"__ignoreMap":508},[512,513,516],"span",{"class":514,"line":515},"line",1,[512,517,519],{"class":518},"sJ8bj","\u002F\u002F axe-custom-rules.ts\n",[512,521,523,527,531,534,538],{"class":514,"line":522},2,[512,524,526],{"class":525},"szBVR","import",[512,528,530],{"class":529},"sVt8B"," axe ",[512,532,533],{"class":525},"from",[512,535,537],{"class":536},"sZZnC"," 'axe-core'",[512,539,540],{"class":529},";\n",[512,542,544],{"class":514,"line":543},3,[512,545,547],{"emptyLinePlaceholder":546},true,"\n",[512,549,551,554,558],{"class":514,"line":550},4,[512,552,553],{"class":529},"axe.",[512,555,557],{"class":556},"sScJk","configure",[512,559,560],{"class":529},"({\n",[512,562,564],{"class":514,"line":563},5,[512,565,566],{"class":518},"  \u002F\u002F 1) The check: the actual decision logic.\n",[512,568,570],{"class":514,"line":569},6,[512,571,572],{"class":529},"  checks: [\n",[512,574,576],{"class":514,"line":575},7,[512,577,578],{"class":529},"    {\n",[512,580,582,585,588],{"class":514,"line":581},8,[512,583,584],{"class":529},"      id: ",[512,586,587],{"class":536},"'icon-button-has-name'",[512,589,590],{"class":529},",\n",[512,592,594],{"class":514,"line":593},9,[512,595,596],{"class":518},"      \u002F\u002F evaluate runs in the page context against each matched node.\n",[512,598,600,603,606,610,613,616],{"class":514,"line":599},10,[512,601,602],{"class":556},"      evaluate",[512,604,605],{"class":529},"(",[512,607,609],{"class":608},"s4XuR","node",[512,611,612],{"class":525},":",[512,614,615],{"class":556}," HTMLElement",[512,617,618],{"class":529},") {\n",[512,620,622],{"class":514,"line":621},11,[512,623,624],{"class":518},"        \u002F\u002F axe.commons exposes the same accessible-name computation\n",[512,626,628],{"class":514,"line":627},12,[512,629,630],{"class":518},"        \u002F\u002F the engine uses for 4.1.2 Name, Role, Value.\n",[512,632,634,637,641,644,647,650,653,656,659],{"class":514,"line":633},13,[512,635,636],{"class":525},"        const",[512,638,640],{"class":639},"sj4cs"," name",[512,642,643],{"class":525}," =",[512,645,646],{"class":529}," (axe ",[512,648,649],{"class":525},"as",[512,651,652],{"class":639}," any",[512,654,655],{"class":529},").commons.text.",[512,657,658],{"class":556},"accessibleText",[512,660,661],{"class":529},"(node);\n",[512,663,665,668,671,674,677,680,683,686,689,692,695,698,701],{"class":514,"line":664},14,[512,666,667],{"class":525},"        return",[512,669,670],{"class":525}," typeof",[512,672,673],{"class":529}," name ",[512,675,676],{"class":525},"===",[512,678,679],{"class":536}," 'string'",[512,681,682],{"class":525}," &&",[512,684,685],{"class":529}," name.",[512,687,688],{"class":556},"trim",[512,690,691],{"class":529},"().",[512,693,694],{"class":639},"length",[512,696,697],{"class":525}," >",[512,699,700],{"class":639}," 0",[512,702,540],{"class":529},[512,704,706],{"class":514,"line":705},15,[512,707,708],{"class":529},"      },\n",[512,710,712],{"class":514,"line":711},16,[512,713,714],{"class":518},"      \u002F\u002F Message shown for pass\u002Ffail in the result data.\n",[512,716,718],{"class":514,"line":717},17,[512,719,720],{"class":529},"      metadata: {\n",[512,722,724,727,730],{"class":514,"line":723},18,[512,725,726],{"class":529},"        impact: ",[512,728,729],{"class":536},"'serious'",[512,731,590],{"class":529},[512,733,735],{"class":514,"line":734},19,[512,736,737],{"class":529},"        messages: {\n",[512,739,741,744,747],{"class":514,"line":740},20,[512,742,743],{"class":529},"          pass: ",[512,745,746],{"class":536},"'Icon button exposes an accessible name.'",[512,748,590],{"class":529},[512,750,752,755,758],{"class":514,"line":751},21,[512,753,754],{"class":529},"          fail: ",[512,756,757],{"class":536},"'Icon-only button has no accessible name (aria-label, aria-labelledby, or visually-hidden text).'",[512,759,590],{"class":529},[512,761,763],{"class":514,"line":762},22,[512,764,765],{"class":529},"        },\n",[512,767,769],{"class":514,"line":768},23,[512,770,708],{"class":529},[512,772,774],{"class":514,"line":773},24,[512,775,776],{"class":529},"    },\n",[512,778,780],{"class":514,"line":779},25,[512,781,782],{"class":529},"  ],\n",[512,784,786],{"class":514,"line":785},26,[512,787,547],{"emptyLinePlaceholder":546},[512,789,791],{"class":514,"line":790},27,[512,792,793],{"class":518},"  \u002F\u002F 2) The rule: bind the check to the nodes it should run on.\n",[512,795,797],{"class":514,"line":796},28,[512,798,799],{"class":529},"  rules: [\n",[512,801,803],{"class":514,"line":802},29,[512,804,578],{"class":529},[512,806,808,810,813],{"class":514,"line":807},30,[512,809,584],{"class":529},[512,811,812],{"class":536},"'ds-icon-button-name'",[512,814,590],{"class":529},[512,816,818],{"class":514,"line":817},31,[512,819,820],{"class":518},"      \u002F\u002F Only our design-system icon buttons match.\n",[512,822,824,827,830],{"class":514,"line":823},32,[512,825,826],{"class":529},"      selector: ",[512,828,829],{"class":536},"'button[data-icon-button], [role=\"button\"][data-icon-button]'",[512,831,590],{"class":529},[512,833,835],{"class":514,"line":834},33,[512,836,837],{"class":518},"      \u002F\u002F Optional secondary predicate for finer matching.\n",[512,839,841,844,847,849,851,853,856,859,862,865,867,870,872,875,878],{"class":514,"line":840},34,[512,842,843],{"class":556},"      matches",[512,845,846],{"class":529},": (",[512,848,609],{"class":608},[512,850,612],{"class":525},[512,852,615],{"class":556},[512,854,855],{"class":529},") ",[512,857,858],{"class":525},"=>",[512,860,861],{"class":529}," node.",[512,863,864],{"class":556},"querySelector",[512,866,605],{"class":529},[512,868,869],{"class":536},"'svg'",[512,871,855],{"class":529},[512,873,874],{"class":525},"!==",[512,876,877],{"class":639}," null",[512,879,590],{"class":529},[512,881,883],{"class":514,"line":882},35,[512,884,885],{"class":518},"      \u002F\u002F Tag it so teams can include\u002Fexclude it via runOnly.\n",[512,887,889,892,895,897,900,902,905],{"class":514,"line":888},36,[512,890,891],{"class":529},"      tags: [",[512,893,894],{"class":536},"'cat.name-role-value'",[512,896,452],{"class":529},[512,898,899],{"class":536},"'best-practice'",[512,901,452],{"class":529},[512,903,904],{"class":536},"'ds-internal'",[512,906,907],{"class":529},"],\n",[512,909,911],{"class":514,"line":910},37,[512,912,913],{"class":518},"      \u002F\u002F 'any' = passes if any listed check passes.\n",[512,915,917,920,922],{"class":514,"line":916},38,[512,918,919],{"class":529},"      any: [",[512,921,587],{"class":536},[512,923,907],{"class":529},[512,925,927],{"class":514,"line":926},39,[512,928,720],{"class":529},[512,930,932,935,938],{"class":514,"line":931},40,[512,933,934],{"class":529},"        description: ",[512,936,937],{"class":536},"'Design-system icon buttons must have an accessible name.'",[512,939,590],{"class":529},[512,941,943,946,949],{"class":514,"line":942},41,[512,944,945],{"class":529},"        help: ",[512,947,948],{"class":536},"'Add aria-label or visually-hidden text to icon-only buttons.'",[512,950,590],{"class":529},[512,952,954,957,960],{"class":514,"line":953},42,[512,955,956],{"class":529},"        helpUrl: ",[512,958,959],{"class":536},"'https:\u002F\u002Finternal.docs\u002Fa11y\u002Ficon-button-name'",[512,961,590],{"class":529},[512,963,965],{"class":514,"line":964},43,[512,966,708],{"class":529},[512,968,970],{"class":514,"line":969},44,[512,971,776],{"class":529},[512,973,975],{"class":514,"line":974},45,[512,976,782],{"class":529},[512,978,980],{"class":514,"line":979},46,[512,981,982],{"class":529},"});\n",[324,984,985,986,989],{},"Run it like any other rule—target your custom tag to run ",[498,987,988],{},"only"," your rules, or merge it into a full audit:",[503,991,993],{"className":505,"code":992,"language":507,"meta":508,"style":508},"\u002F\u002F Run only design-system rules during a focused check.\nconst results = await axe.run(document, {\n  runOnly: { type: 'tag', values: ['ds-internal'] },\n});\nconsole.log(results.violations.filter((v) => v.id === 'ds-icon-button-name'));\n",[328,994,995,1000,1022,1038,1042],{"__ignoreMap":508},[512,996,997],{"class":514,"line":515},[512,998,999],{"class":518},"\u002F\u002F Run only design-system rules during a focused check.\n",[512,1001,1002,1005,1008,1010,1013,1016,1019],{"class":514,"line":522},[512,1003,1004],{"class":525},"const",[512,1006,1007],{"class":639}," results",[512,1009,643],{"class":525},[512,1011,1012],{"class":525}," await",[512,1014,1015],{"class":529}," axe.",[512,1017,1018],{"class":556},"run",[512,1020,1021],{"class":529},"(document, {\n",[512,1023,1024,1027,1030,1033,1035],{"class":514,"line":543},[512,1025,1026],{"class":529},"  runOnly: { type: ",[512,1028,1029],{"class":536},"'tag'",[512,1031,1032],{"class":529},", values: [",[512,1034,904],{"class":536},[512,1036,1037],{"class":529},"] },\n",[512,1039,1040],{"class":514,"line":550},[512,1041,982],{"class":529},[512,1043,1044,1047,1050,1053,1056,1059,1062,1064,1066,1069,1071,1074],{"class":514,"line":563},[512,1045,1046],{"class":529},"console.",[512,1048,1049],{"class":556},"log",[512,1051,1052],{"class":529},"(results.violations.",[512,1054,1055],{"class":556},"filter",[512,1057,1058],{"class":529},"((",[512,1060,1061],{"class":608},"v",[512,1063,855],{"class":529},[512,1065,858],{"class":525},[512,1067,1068],{"class":529}," v.id ",[512,1070,676],{"class":525},[512,1072,1073],{"class":536}," 'ds-icon-button-name'",[512,1075,1076],{"class":529},"));\n",[324,1078,1079,1081,1082,1085,1086,1089],{},[328,1080,338],{}," is ",[341,1083,1084],{},"global and replaces configuration",", so call it once at setup. Re-running it discards prior custom config; in tests, use ",[328,1087,1088],{},"axe.reset()"," between suites if you reconfigure.",[399,1091],{},[355,1093,1095],{"id":1094},"enabling-and-disabling-rules","Enabling and Disabling Rules",[324,1097,1098,1099,1101,1102,1104],{},"You don't always need a new check to change behaviour. ",[328,1100,338],{}," (and ",[328,1103,1018],{}," options) can toggle existing rules and adjust their tags:",[503,1106,1108],{"className":505,"code":1107,"language":507,"meta":508,"style":508},"import axe from 'axe-core';\n\naxe.configure({\n  rules: [\n    \u002F\u002F Turn ON a best-practice rule that's off by default.\n    { id: 'region', enabled: true },\n    \u002F\u002F Turn OFF a rule that conflicts with a known, documented exception.\n    { id: 'duplicate-id', enabled: false },\n    \u002F\u002F Re-tag an existing rule so it runs under your gate's tag set.\n    { id: 'color-contrast', tags: ['wcag2aa', 'wcag21aa', 'ds-internal'] },\n  ],\n});\n\n\u002F\u002F Per-run override without changing global config:\nawait axe.run(document, { rules: { 'region': { enabled: false } } });\n",[328,1109,1110,1122,1126,1134,1138,1143,1159,1164,1177,1182,1206,1210,1214,1218,1223],{"__ignoreMap":508},[512,1111,1112,1114,1116,1118,1120],{"class":514,"line":515},[512,1113,526],{"class":525},[512,1115,530],{"class":529},[512,1117,533],{"class":525},[512,1119,537],{"class":536},[512,1121,540],{"class":529},[512,1123,1124],{"class":514,"line":522},[512,1125,547],{"emptyLinePlaceholder":546},[512,1127,1128,1130,1132],{"class":514,"line":543},[512,1129,553],{"class":529},[512,1131,557],{"class":556},[512,1133,560],{"class":529},[512,1135,1136],{"class":514,"line":550},[512,1137,799],{"class":529},[512,1139,1140],{"class":514,"line":563},[512,1141,1142],{"class":518},"    \u002F\u002F Turn ON a best-practice rule that's off by default.\n",[512,1144,1145,1148,1151,1154,1156],{"class":514,"line":569},[512,1146,1147],{"class":529},"    { id: ",[512,1149,1150],{"class":536},"'region'",[512,1152,1153],{"class":529},", enabled: ",[512,1155,425],{"class":639},[512,1157,1158],{"class":529}," },\n",[512,1160,1161],{"class":514,"line":575},[512,1162,1163],{"class":518},"    \u002F\u002F Turn OFF a rule that conflicts with a known, documented exception.\n",[512,1165,1166,1168,1171,1173,1175],{"class":514,"line":581},[512,1167,1147],{"class":529},[512,1169,1170],{"class":536},"'duplicate-id'",[512,1172,1153],{"class":529},[512,1174,429],{"class":639},[512,1176,1158],{"class":529},[512,1178,1179],{"class":514,"line":593},[512,1180,1181],{"class":518},"    \u002F\u002F Re-tag an existing rule so it runs under your gate's tag set.\n",[512,1183,1184,1186,1189,1192,1195,1197,1200,1202,1204],{"class":514,"line":599},[512,1185,1147],{"class":529},[512,1187,1188],{"class":536},"'color-contrast'",[512,1190,1191],{"class":529},", tags: [",[512,1193,1194],{"class":536},"'wcag2aa'",[512,1196,452],{"class":529},[512,1198,1199],{"class":536},"'wcag21aa'",[512,1201,452],{"class":529},[512,1203,904],{"class":536},[512,1205,1037],{"class":529},[512,1207,1208],{"class":514,"line":621},[512,1209,782],{"class":529},[512,1211,1212],{"class":514,"line":627},[512,1213,982],{"class":529},[512,1215,1216],{"class":514,"line":633},[512,1217,547],{"emptyLinePlaceholder":546},[512,1219,1220],{"class":514,"line":664},[512,1221,1222],{"class":518},"\u002F\u002F Per-run override without changing global config:\n",[512,1224,1225,1228,1230,1232,1235,1237,1240,1242],{"class":514,"line":705},[512,1226,1227],{"class":525},"await",[512,1229,1015],{"class":529},[512,1231,1018],{"class":556},[512,1233,1234],{"class":529},"(document, { rules: { ",[512,1236,1150],{"class":536},[512,1238,1239],{"class":529},": { enabled: ",[512,1241,429],{"class":639},[512,1243,1244],{"class":529}," } } });\n",[324,1246,1247,1248,1250],{},"Prefer per-run overrides for one-off scoping and reserve ",[328,1249,338],{}," for permanent, project-wide policy. Document every disable with a reason and owner so it surfaces in review.",[399,1252],{},[355,1254,1256],{"id":1255},"testing-the-custom-rule","Testing the Custom Rule",[324,1258,1259],{},"A custom rule is code; it needs its own tests against known-good and known-bad DOM. Drive it through jest-axe or the raw API so a logic bug doesn't silently pass everything.",[503,1261,1263],{"className":505,"code":1262,"language":507,"meta":508,"style":508},"\u002F\u002F ds-icon-button-name.test.ts\nimport axe from 'axe-core';\nimport '.\u002Faxe-custom-rules'; \u002F\u002F registers the rule via axe.configure()\n\nfunction mount(html: string) {\n  document.body.innerHTML = html;\n  return document.body;\n}\n\nit('fails an icon button with no accessible name', async () => {\n  mount(`\u003Cbutton data-icon-button>\u003Csvg aria-hidden=\"true\">\u003C\u002Fsvg>\u003C\u002Fbutton>`);\n  const results = await axe.run(document.body, {\n    runOnly: { type: 'tag', values: ['ds-internal'] },\n  });\n  const ids = results.violations.map((v) => v.id);\n  expect(ids).toContain('ds-icon-button-name');\n});\n\nit('passes an icon button with aria-label', async () => {\n  mount(`\u003Cbutton data-icon-button aria-label=\"Close\">\u003Csvg aria-hidden=\"true\">\u003C\u002Fsvg>\u003C\u002Fbutton>`);\n  const results = await axe.run(document.body, {\n    runOnly: { type: 'tag', values: ['ds-internal'] },\n  });\n  expect(results.violations.map((v) => v.id)).not.toContain('ds-icon-button-name');\n});\n\nit('does not match plain buttons (selector scoping)', async () => {\n  mount(`\u003Cbutton>Save\u003C\u002Fbutton>`); \u002F\u002F no data-icon-button → rule inapplicable\n  const results = await axe.run(document.body, {\n    runOnly: { type: 'tag', values: ['ds-internal'] },\n  });\n  expect(results.violations.map((v) => v.id)).not.toContain('ds-icon-button-name');\n});\n",[328,1264,1265,1270,1282,1295,1299,1319,1330,1338,1343,1347,1370,1383,1401,1414,1419,1445,1462,1466,1470,1489,1500,1516,1528,1532,1559,1563,1567,1586,1601,1617,1629,1633,1659],{"__ignoreMap":508},[512,1266,1267],{"class":514,"line":515},[512,1268,1269],{"class":518},"\u002F\u002F ds-icon-button-name.test.ts\n",[512,1271,1272,1274,1276,1278,1280],{"class":514,"line":522},[512,1273,526],{"class":525},[512,1275,530],{"class":529},[512,1277,533],{"class":525},[512,1279,537],{"class":536},[512,1281,540],{"class":529},[512,1283,1284,1286,1289,1292],{"class":514,"line":543},[512,1285,526],{"class":525},[512,1287,1288],{"class":536}," '.\u002Faxe-custom-rules'",[512,1290,1291],{"class":529},"; ",[512,1293,1294],{"class":518},"\u002F\u002F registers the rule via axe.configure()\n",[512,1296,1297],{"class":514,"line":550},[512,1298,547],{"emptyLinePlaceholder":546},[512,1300,1301,1304,1307,1309,1312,1314,1317],{"class":514,"line":563},[512,1302,1303],{"class":525},"function",[512,1305,1306],{"class":556}," mount",[512,1308,605],{"class":529},[512,1310,1311],{"class":608},"html",[512,1313,612],{"class":525},[512,1315,1316],{"class":639}," string",[512,1318,618],{"class":529},[512,1320,1321,1324,1327],{"class":514,"line":569},[512,1322,1323],{"class":529},"  document.body.innerHTML ",[512,1325,1326],{"class":525},"=",[512,1328,1329],{"class":529}," html;\n",[512,1331,1332,1335],{"class":514,"line":575},[512,1333,1334],{"class":525},"  return",[512,1336,1337],{"class":529}," document.body;\n",[512,1339,1340],{"class":514,"line":581},[512,1341,1342],{"class":529},"}\n",[512,1344,1345],{"class":514,"line":593},[512,1346,547],{"emptyLinePlaceholder":546},[512,1348,1349,1352,1354,1357,1359,1362,1365,1367],{"class":514,"line":599},[512,1350,1351],{"class":556},"it",[512,1353,605],{"class":529},[512,1355,1356],{"class":536},"'fails an icon button with no accessible name'",[512,1358,452],{"class":529},[512,1360,1361],{"class":525},"async",[512,1363,1364],{"class":529}," () ",[512,1366,858],{"class":525},[512,1368,1369],{"class":529}," {\n",[512,1371,1372,1375,1377,1380],{"class":514,"line":621},[512,1373,1374],{"class":556},"  mount",[512,1376,605],{"class":529},[512,1378,1379],{"class":536},"`\u003Cbutton data-icon-button>\u003Csvg aria-hidden=\"true\">\u003C\u002Fsvg>\u003C\u002Fbutton>`",[512,1381,1382],{"class":529},");\n",[512,1384,1385,1388,1390,1392,1394,1396,1398],{"class":514,"line":627},[512,1386,1387],{"class":525},"  const",[512,1389,1007],{"class":639},[512,1391,643],{"class":525},[512,1393,1012],{"class":525},[512,1395,1015],{"class":529},[512,1397,1018],{"class":556},[512,1399,1400],{"class":529},"(document.body, {\n",[512,1402,1403,1406,1408,1410,1412],{"class":514,"line":633},[512,1404,1405],{"class":529},"    runOnly: { type: ",[512,1407,1029],{"class":536},[512,1409,1032],{"class":529},[512,1411,904],{"class":536},[512,1413,1037],{"class":529},[512,1415,1416],{"class":514,"line":664},[512,1417,1418],{"class":529},"  });\n",[512,1420,1421,1423,1426,1428,1431,1434,1436,1438,1440,1442],{"class":514,"line":705},[512,1422,1387],{"class":525},[512,1424,1425],{"class":639}," ids",[512,1427,643],{"class":525},[512,1429,1430],{"class":529}," results.violations.",[512,1432,1433],{"class":556},"map",[512,1435,1058],{"class":529},[512,1437,1061],{"class":608},[512,1439,855],{"class":529},[512,1441,858],{"class":525},[512,1443,1444],{"class":529}," v.id);\n",[512,1446,1447,1450,1453,1456,1458,1460],{"class":514,"line":711},[512,1448,1449],{"class":556},"  expect",[512,1451,1452],{"class":529},"(ids).",[512,1454,1455],{"class":556},"toContain",[512,1457,605],{"class":529},[512,1459,812],{"class":536},[512,1461,1382],{"class":529},[512,1463,1464],{"class":514,"line":717},[512,1465,982],{"class":529},[512,1467,1468],{"class":514,"line":723},[512,1469,547],{"emptyLinePlaceholder":546},[512,1471,1472,1474,1476,1479,1481,1483,1485,1487],{"class":514,"line":734},[512,1473,1351],{"class":556},[512,1475,605],{"class":529},[512,1477,1478],{"class":536},"'passes an icon button with aria-label'",[512,1480,452],{"class":529},[512,1482,1361],{"class":525},[512,1484,1364],{"class":529},[512,1486,858],{"class":525},[512,1488,1369],{"class":529},[512,1490,1491,1493,1495,1498],{"class":514,"line":740},[512,1492,1374],{"class":556},[512,1494,605],{"class":529},[512,1496,1497],{"class":536},"`\u003Cbutton data-icon-button aria-label=\"Close\">\u003Csvg aria-hidden=\"true\">\u003C\u002Fsvg>\u003C\u002Fbutton>`",[512,1499,1382],{"class":529},[512,1501,1502,1504,1506,1508,1510,1512,1514],{"class":514,"line":751},[512,1503,1387],{"class":525},[512,1505,1007],{"class":639},[512,1507,643],{"class":525},[512,1509,1012],{"class":525},[512,1511,1015],{"class":529},[512,1513,1018],{"class":556},[512,1515,1400],{"class":529},[512,1517,1518,1520,1522,1524,1526],{"class":514,"line":762},[512,1519,1405],{"class":529},[512,1521,1029],{"class":536},[512,1523,1032],{"class":529},[512,1525,904],{"class":536},[512,1527,1037],{"class":529},[512,1529,1530],{"class":514,"line":768},[512,1531,1418],{"class":529},[512,1533,1534,1536,1538,1540,1542,1544,1546,1548,1551,1553,1555,1557],{"class":514,"line":773},[512,1535,1449],{"class":556},[512,1537,1052],{"class":529},[512,1539,1433],{"class":556},[512,1541,1058],{"class":529},[512,1543,1061],{"class":608},[512,1545,855],{"class":529},[512,1547,858],{"class":525},[512,1549,1550],{"class":529}," v.id)).not.",[512,1552,1455],{"class":556},[512,1554,605],{"class":529},[512,1556,812],{"class":536},[512,1558,1382],{"class":529},[512,1560,1561],{"class":514,"line":779},[512,1562,982],{"class":529},[512,1564,1565],{"class":514,"line":785},[512,1566,547],{"emptyLinePlaceholder":546},[512,1568,1569,1571,1573,1576,1578,1580,1582,1584],{"class":514,"line":790},[512,1570,1351],{"class":556},[512,1572,605],{"class":529},[512,1574,1575],{"class":536},"'does not match plain buttons (selector scoping)'",[512,1577,452],{"class":529},[512,1579,1361],{"class":525},[512,1581,1364],{"class":529},[512,1583,858],{"class":525},[512,1585,1369],{"class":529},[512,1587,1588,1590,1592,1595,1598],{"class":514,"line":796},[512,1589,1374],{"class":556},[512,1591,605],{"class":529},[512,1593,1594],{"class":536},"`\u003Cbutton>Save\u003C\u002Fbutton>`",[512,1596,1597],{"class":529},"); ",[512,1599,1600],{"class":518},"\u002F\u002F no data-icon-button → rule inapplicable\n",[512,1602,1603,1605,1607,1609,1611,1613,1615],{"class":514,"line":802},[512,1604,1387],{"class":525},[512,1606,1007],{"class":639},[512,1608,643],{"class":525},[512,1610,1012],{"class":525},[512,1612,1015],{"class":529},[512,1614,1018],{"class":556},[512,1616,1400],{"class":529},[512,1618,1619,1621,1623,1625,1627],{"class":514,"line":807},[512,1620,1405],{"class":529},[512,1622,1029],{"class":536},[512,1624,1032],{"class":529},[512,1626,904],{"class":536},[512,1628,1037],{"class":529},[512,1630,1631],{"class":514,"line":817},[512,1632,1418],{"class":529},[512,1634,1635,1637,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657],{"class":514,"line":823},[512,1636,1449],{"class":556},[512,1638,1052],{"class":529},[512,1640,1433],{"class":556},[512,1642,1058],{"class":529},[512,1644,1061],{"class":608},[512,1646,855],{"class":529},[512,1648,858],{"class":525},[512,1650,1550],{"class":529},[512,1652,1455],{"class":556},[512,1654,605],{"class":529},[512,1656,812],{"class":536},[512,1658,1382],{"class":529},[512,1660,1661],{"class":514,"line":834},[512,1662,982],{"class":529},[324,1664,1665,1666,1669,1670,86,1672,1674,1675,1677,1678,397],{},"Test all three outcomes: a fail, a pass, and an ",[341,1667,1668],{},"inapplicable"," case proving your ",[328,1671,443],{},[328,1673,447],{}," scoping doesn't leak onto unrelated nodes. If your check can return ",[328,1676,433],{},", add a fourth test asserting it lands in ",[328,1679,389],{},[324,1681,1682,1683,1687],{},"For verifying the real components rendered by your framework, run the same registered rule inside ",[350,1684,1686],{"href":1685},"\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002F","component testing with jest-axe"," so it gates actual JSX output, not just hand-written HTML fixtures.",[399,1689],{},[355,1691,1693],{"id":1692},"how-to-verify","How to Verify",[360,1695,1696,1711,1725],{},[363,1697,1698,1701,1702,1704,1705,1707,1708,1710],{},[341,1699,1700],{},"Automated:"," run the rule against fixtures and against a real rendered component; confirm it appears in ",[328,1703,385],{}," for bad input, ",[328,1706,392],{}," for good input, and ",[328,1709,1668],{}," when the selector shouldn't match.",[363,1712,1713,1716,1717,1720,1721,1724],{},[341,1714,1715],{},"Cross-check the engine version:"," custom config is tied to the loaded axe build. Log ",[328,1718,1719],{},"results.testEngine.version"," in CI and re-validate your rule when you upgrade axe-core—internal helpers like ",[328,1722,1723],{},"axe.commons"," are not a frozen public API.",[363,1726,1727,1730,1731,1733,1734,1737],{},[341,1728,1729],{},"Manual:"," for any rule about accessible names or roles, confirm with a screen reader that the thing your rule enforces actually produces the intended announcement. A rule that checks for ",[328,1732,489],{}," presence still can't tell you the label is ",[498,1735,1736],{},"meaningful","—verify that by ear.",[399,1739],{},[355,1741,1743],{"id":1742},"when-a-lint-rule-or-jest-axe-is-better","When a Lint Rule or jest-axe Is Better",[324,1745,1746],{},"Custom axe rules are powerful but not always the right layer. Choose deliberately:",[360,1748,1749,1775,1791],{},[363,1750,1751,1758,1759,1762,1763,1766,1767,1770,1771,1774],{},[341,1752,1753,1754,1757],{},"Reach for a lint rule (ESLint, ",[328,1755,1756],{},"eslint-plugin-jsx-a11y",") when the defect is visible in source."," Missing ",[328,1760,1761],{},"alt"," on an ",[328,1764,1765],{},"\u003Cimg>",", a static ",[328,1768,1769],{},"onClick"," on a ",[328,1772,1773],{},"\u003Cdiv>","—catching these at author time, before render, is faster and needs no runtime. axe can't see source; lint can't see the rendered\u002Fhydrated DOM. They're complementary.",[363,1776,1777,1780,1781,1783,1784,1786,1787,1790],{},[341,1778,1779],{},"Reach for a plain jest-axe assertion when the requirement is one-off or component-local."," If only your ",[328,1782,330],{}," needs ",[328,1785,334],{},", a ",[328,1788,1789],{},"expect(container.querySelector('[role=dialog]')).toHaveAttribute('aria-labelledby')"," assertion in that component's test is simpler than a global rule and lives next to the component.",[363,1792,1793,1796,1797,1800,1801,1803],{},[341,1794,1795],{},"Reach for a custom axe rule when the requirement is cross-cutting and runtime-dependent."," A convention that must hold across ",[498,1798,1799],{},"every"," page and only manifests in the live DOM (computed names, applied roles, conditionally rendered ARIA) is exactly what ",[328,1802,338],{}," is for—register once, run everywhere, including E2E.",[324,1805,1806],{},"A useful heuristic: if it's a source pattern, lint it; if it's one component, assert it; if it's a system-wide runtime invariant, make it an axe rule.",[399,1808],{},[355,1810,1812],{"id":1811},"common-a11y-mistakes","Common a11y Mistakes",[1814,1815,1816,1828,1837,1852,1861],"ol",{},[363,1817,1818,1824,1825,1827],{},[341,1819,1820,1821,1823],{},"Calling ",[328,1822,338],{}," more than once."," It replaces, not merges. Register all custom rules in a single setup module and ",[328,1826,1088],{}," between test suites if needed.",[363,1829,1830,1833,1834,1836],{},[341,1831,1832],{},"Forgetting to scope the selector."," A too-broad ",[328,1835,443],{}," makes your rule fire on unrelated nodes and flood reports. Test the inapplicable case.",[363,1838,1839,1845,1846,1848,1849,1851],{},[341,1840,1841,1842,1844],{},"Returning ",[328,1843,429],{}," when you mean \"can't tell.\""," If the check genuinely can't decide, return ",[328,1847,433],{}," so the node lands in ",[328,1850,389],{},"—don't fabricate a pass or fail.",[363,1853,1854,1860],{},[341,1855,1856,1857,1859],{},"Depending on ",[328,1858,1723],{}," as a stable API."," Internal helpers can change between versions; pin your axe-core version and re-test custom rules on upgrade.",[363,1862,1863,1866,1867,1869],{},[341,1864,1865],{},"Building a custom rule for something lint already catches."," Source-visible issues belong in ",[328,1868,1756],{},", not a runtime rule.",[399,1871],{},[355,1873,1875],{"id":1874},"conclusion","Conclusion",[324,1877,1878,1880],{},[328,1879,338],{}," turns axe-core from a fixed checker into an extensible policy engine for your design system: a check holds the logic, a rule scopes and labels it, and both flow through the same violations\u002Fincomplete\u002Fpasses model your team already reads. Author the rule, scope its selector tightly, test all three outcomes, and re-validate on every axe upgrade. Reserve custom rules for cross-cutting runtime invariants—let lint catch source patterns and jest-axe assertions handle the one-offs.",[399,1882],{},[355,1884,1886],{"id":1885},"frequently-asked-questions","Frequently Asked Questions",[324,1888,1889,1892,1893,1895,1896,1898,1899,1901,1902,1905,1906,1909],{},[341,1890,1891],{},"What's the difference between a check and a rule in axe-core?","\nA check is the ",[328,1894,375],{}," function that inspects a single node and returns true, false, or undefined. A rule binds one or more checks to nodes via a CSS ",[328,1897,443],{}," and carries metadata and tags. You register both with ",[328,1900,338],{},"; the rule decides ",[498,1903,1904],{},"which"," nodes get tested, the check decides ",[498,1907,1908],{},"whether"," each passes.",[324,1911,1912,1915,1916,1918,1919,1921],{},[341,1913,1914],{},"Does axe.configure() merge with existing custom rules?","\nNo. ",[328,1917,338],{}," replaces the active configuration each time it's called. Register all your custom checks and rules in one setup module, and call ",[328,1920,1088],{}," to return to defaults—important between test suites that reconfigure differently.",[324,1923,1924,1927,1928,1930,1931,1933],{},[341,1925,1926],{},"When should I write a custom axe rule instead of an ESLint rule?","\nUse ESLint (",[328,1929,1756],{},") when the issue is visible in source code, like a missing ",[328,1932,1761],{}," attribute—it's caught before render with no runtime cost. Use a custom axe rule when the requirement depends on the rendered, hydrated DOM (computed accessible names, applied roles, conditional ARIA) and must hold across the whole app.",[324,1935,1936,1939,1940,1942,1943,1945,1946,1948,1949,1952],{},[341,1937,1938],{},"Can a custom check return a \"needs review\" result?","\nYes. Return ",[328,1941,433],{}," from ",[328,1944,375],{}," and axe classifies the node as ",[328,1947,389],{}," rather than pass or fail. Use this when your logic genuinely can't decide automatically—the same mechanism the built-in ",[328,1950,1951],{},"color-contrast"," rule uses over images and gradients.",[399,1954],{},[355,1956,1958],{"id":1957},"related-guides","Related guides",[360,1960,1961,1966,1972],{},[363,1962,1963,1965],{},[350,1964,243],{"href":352}," — the guide on the rules engine.",[363,1967,1968,1971],{},[350,1969,249],{"href":1970},"\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core\u002F"," — interpret the built-in contrast rule and its incompletes.",[363,1973,1974,1976],{},[350,1975,261],{"href":1685}," — run your registered rules against real rendered components in CI.",[1978,1979,1980],"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 .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 .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}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);}",{"title":508,"searchDepth":522,"depth":522,"links":1982},[1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993],{"id":357,"depth":522,"text":358},{"id":403,"depth":522,"text":404},{"id":478,"depth":522,"text":479},{"id":1094,"depth":522,"text":1095},{"id":1255,"depth":522,"text":1256},{"id":1692,"depth":522,"text":1693},{"id":1742,"depth":522,"text":1743},{"id":1811,"depth":522,"text":1812},{"id":1874,"depth":522,"text":1875},{"id":1885,"depth":522,"text":1886},{"id":1957,"depth":522,"text":1958},null,"Extend axe-core with project-specific accessibility rules—register custom checks and rules with axe.configure(), write evaluate functions, and scope them to your components.","md",{},false,{"title":255,"description":1995},"0eb29HPeRsFGqxeHJqeEk0NKMMEohd9VZ8NnIouBf24",[2002,2041,2042,2105],{"title":5,"path":6,"stem":7,"children":2003},[2004,2005,2008,2011,2017,2023,2032,2038],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2006},[2007],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2009},[2010],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2012},[2013,2014],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2015},[2016],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2018},[2019,2020],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2021},[2022],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2024},[2025,2026,2029],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2027},[2028],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2030},[2031],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2033},[2034,2035],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2036},[2037],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2039},[2040],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2043},[2044,2045,2051,2063,2075,2078,2087,2099],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2046},[2047,2048],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2049},[2050],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2052},[2053,2054,2057,2060],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2055},[2056],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2058},[2059],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2061},[2062],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2064},[2065,2066,2069,2072],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2067},[2068],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2070},[2071],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2073},[2074],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2076},[2077],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2079},[2080,2081,2084],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2082},[2083],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2085},[2086],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2088},[2089,2090,2093,2096],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2091},[2092],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2094},[2095],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2097},[2098],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2100},[2101,2102],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2103},[2104],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2106},[2107,2108,2117,2126,2135,2144],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2109},[2110,2111,2114],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2112},[2113],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2115},[2116],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2118},[2119,2120,2123],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2121},[2122],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2124},[2125],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2127},[2128,2129,2132],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2130},[2131],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2133},[2134],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2136},[2137,2138,2141],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2139},[2140],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2142},[2143],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2145},[2146,2147,2150],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2148},[2149],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2151},[2152],{"title":309,"path":310,"stem":311},1781785524229]