[{"data":1,"prerenderedAt":2025},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core\u002F":314,"content-navigation":1873},[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":249,"body":316,"date":1866,"description":1867,"extension":1868,"image":1866,"meta":1869,"modifiedAt":1866,"navigation":455,"noindex":1870,"path":250,"publishedAt":1866,"seo":1871,"stem":251,"updatedAt":1866,"__hash__":1872},"content\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core\u002Findex.md",{"type":317,"value":318,"toc":1855},"minimark",[319,323,342,347,377,380,384,390,407,418,645,648,738,751,753,757,767,800,812,982,989,991,995,998,1434,1513,1516,1536,1541,1543,1547,1550,1555,1666,1671,1697,1704,1706,1710,1762,1764,1768,1774,1776,1780,1792,1804,1819,1827,1829,1833,1851],[320,321,249],"h1",{"id":322},"catching-color-contrast-failures-with-axe-core",[324,325,326,327,331,332,336,337,341],"p",{},"Color contrast is the single most common WCAG failure in production, and it's also one of the few that automation handles well—when the background is a solid color. axe-core's ",[328,329,330],"code",{},"color-contrast"," rule computes the ratio between foreground text and its resolved background and fails anything below the threshold. The friction starts when text sits over a gradient, an image, or a semi-transparent overlay: there axe returns ",[333,334,335],"strong",{},"incomplete"," (\"needs review\") rather than a verdict, and teams routinely misread that as a pass. This guide, part of ",[338,339,243],"a",{"href":340},"\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002F",", explains exactly how the rule decides, why it punts on certain backgrounds, and how to fix the underlying design tokens.",[343,344,346],"h2",{"id":345},"prerequisites","Prerequisites",[348,349,350,358,366],"ul",{},[351,352,353,354,357],"li",{},"axe-core wired into your dev loop or test suite (browser extension, ",[328,355,356],{},"@axe-core\u002Freact",", jest-axe, or Playwright).",[351,359,360,361,365],{},"A theming layer built on CSS custom properties—see ",[338,362,364],{"href":363},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002F","accessible color contrast & theming"," for the token architecture this guide assumes.",[351,367,368,369,372,373,376],{},"Familiarity with the relevant criteria: ",[328,370,371],{},"1.4.3 Contrast (Minimum)"," for text and ",[328,374,375],{},"1.4.11 Non-text Contrast"," for UI components and focus indicators.",[378,379],"hr",{},[343,381,383],{"id":382},"how-the-color-contrast-rule-works","How the color-contrast Rule Works",[324,385,386,387,389],{},"The ",[328,388,330],{}," rule selects rendered text nodes, then for each one it resolves the computed foreground color and walks up the DOM to find the effective background color. It calculates relative luminance for both and produces a ratio, which it tests against the WCAG thresholds:",[348,391,392,401],{},[351,393,394,397,398,400],{},[333,395,396],{},"4.5:1"," for normal text (",[328,399,371],{},").",[351,402,403,406],{},[333,404,405],{},"3:1"," for large text — 24px, or 18.66px (14pt) bold.",[324,408,409,410,413,414,417],{},"The rule only fires on ",[328,411,412],{},"wcag2aa"," tag runs and above, so confirm your ",[328,415,416],{},"runOnly"," includes it:",[419,420,425],"pre",{"className":421,"code":422,"language":423,"meta":424,"style":424},"language-ts shiki shiki-themes github-light github-dark","import axe from 'axe-core';\n\nconst results = await axe.run(document, {\n  runOnly: { type: 'tag', values: ['wcag2aa', 'wcag21aa'] },\n});\n\n\u002F\u002F Separate confident failures from \"you need to look\" results.\nconst contrastViolations = results.violations.filter((v) => v.id === 'color-contrast');\nconst contrastReview = results.incomplete.filter((v) => v.id === 'color-contrast');\n\nconsole.error('definite contrast failures:', contrastViolations.length);\nconsole.warn('contrast needs manual review:', contrastReview.length);\n","ts","",[328,426,427,450,457,483,507,513,518,525,566,597,602,625],{"__ignoreMap":424},[428,429,432,436,440,443,447],"span",{"class":430,"line":431},"line",1,[428,433,435],{"class":434},"szBVR","import",[428,437,439],{"class":438},"sVt8B"," axe ",[428,441,442],{"class":434},"from",[428,444,446],{"class":445},"sZZnC"," 'axe-core'",[428,448,449],{"class":438},";\n",[428,451,453],{"class":430,"line":452},2,[428,454,456],{"emptyLinePlaceholder":455},true,"\n",[428,458,460,463,467,470,473,476,480],{"class":430,"line":459},3,[428,461,462],{"class":434},"const",[428,464,466],{"class":465},"sj4cs"," results",[428,468,469],{"class":434}," =",[428,471,472],{"class":434}," await",[428,474,475],{"class":438}," axe.",[428,477,479],{"class":478},"sScJk","run",[428,481,482],{"class":438},"(document, {\n",[428,484,486,489,492,495,498,501,504],{"class":430,"line":485},4,[428,487,488],{"class":438},"  runOnly: { type: ",[428,490,491],{"class":445},"'tag'",[428,493,494],{"class":438},", values: [",[428,496,497],{"class":445},"'wcag2aa'",[428,499,500],{"class":438},", ",[428,502,503],{"class":445},"'wcag21aa'",[428,505,506],{"class":438},"] },\n",[428,508,510],{"class":430,"line":509},5,[428,511,512],{"class":438},"});\n",[428,514,516],{"class":430,"line":515},6,[428,517,456],{"emptyLinePlaceholder":455},[428,519,521],{"class":430,"line":520},7,[428,522,524],{"class":523},"sJ8bj","\u002F\u002F Separate confident failures from \"you need to look\" results.\n",[428,526,528,530,533,535,538,541,544,548,551,554,557,560,563],{"class":430,"line":527},8,[428,529,462],{"class":434},[428,531,532],{"class":465}," contrastViolations",[428,534,469],{"class":434},[428,536,537],{"class":438}," results.violations.",[428,539,540],{"class":478},"filter",[428,542,543],{"class":438},"((",[428,545,547],{"class":546},"s4XuR","v",[428,549,550],{"class":438},") ",[428,552,553],{"class":434},"=>",[428,555,556],{"class":438}," v.id ",[428,558,559],{"class":434},"===",[428,561,562],{"class":445}," 'color-contrast'",[428,564,565],{"class":438},");\n",[428,567,569,571,574,576,579,581,583,585,587,589,591,593,595],{"class":430,"line":568},9,[428,570,462],{"class":434},[428,572,573],{"class":465}," contrastReview",[428,575,469],{"class":434},[428,577,578],{"class":438}," results.incomplete.",[428,580,540],{"class":478},[428,582,543],{"class":438},[428,584,547],{"class":546},[428,586,550],{"class":438},[428,588,553],{"class":434},[428,590,556],{"class":438},[428,592,559],{"class":434},[428,594,562],{"class":445},[428,596,565],{"class":438},[428,598,600],{"class":430,"line":599},10,[428,601,456],{"emptyLinePlaceholder":455},[428,603,605,608,611,614,617,620,623],{"class":430,"line":604},11,[428,606,607],{"class":438},"console.",[428,609,610],{"class":478},"error",[428,612,613],{"class":438},"(",[428,615,616],{"class":445},"'definite contrast failures:'",[428,618,619],{"class":438},", contrastViolations.",[428,621,622],{"class":465},"length",[428,624,565],{"class":438},[428,626,628,630,633,635,638,641,643],{"class":430,"line":627},12,[428,629,607],{"class":438},[428,631,632],{"class":478},"warn",[428,634,613],{"class":438},[428,636,637],{"class":445},"'contrast needs manual review:'",[428,639,640],{"class":438},", contrastReview.",[428,642,622],{"class":465},[428,644,565],{"class":438},[324,646,647],{},"Each failing node comes with the data axe used, which is what makes the result actionable:",[419,649,651],{"className":421,"code":650,"language":423,"meta":424,"style":424},"contrastViolations.forEach((v) => {\n  v.nodes.forEach((node) => {\n    const data = node.any[0]?.data; \u002F\u002F { fgColor, bgColor, contrastRatio, expectedContrastRatio }\n    console.log(node.target, data?.contrastRatio, 'needs', data?.expectedContrastRatio);\n  });\n});\n",[328,652,653,672,690,712,729,734],{"__ignoreMap":424},[428,654,655,658,661,663,665,667,669],{"class":430,"line":431},[428,656,657],{"class":438},"contrastViolations.",[428,659,660],{"class":478},"forEach",[428,662,543],{"class":438},[428,664,547],{"class":546},[428,666,550],{"class":438},[428,668,553],{"class":434},[428,670,671],{"class":438}," {\n",[428,673,674,677,679,681,684,686,688],{"class":430,"line":452},[428,675,676],{"class":438},"  v.nodes.",[428,678,660],{"class":478},[428,680,543],{"class":438},[428,682,683],{"class":546},"node",[428,685,550],{"class":438},[428,687,553],{"class":434},[428,689,671],{"class":438},[428,691,692,695,698,700,703,706,709],{"class":430,"line":459},[428,693,694],{"class":434},"    const",[428,696,697],{"class":465}," data",[428,699,469],{"class":434},[428,701,702],{"class":438}," node.any[",[428,704,705],{"class":465},"0",[428,707,708],{"class":438},"]?.data; ",[428,710,711],{"class":523},"\u002F\u002F { fgColor, bgColor, contrastRatio, expectedContrastRatio }\n",[428,713,714,717,720,723,726],{"class":430,"line":485},[428,715,716],{"class":438},"    console.",[428,718,719],{"class":478},"log",[428,721,722],{"class":438},"(node.target, data?.contrastRatio, ",[428,724,725],{"class":445},"'needs'",[428,727,728],{"class":438},", data?.expectedContrastRatio);\n",[428,730,731],{"class":430,"line":509},[428,732,733],{"class":438},"  });\n",[428,735,736],{"class":430,"line":515},[428,737,512],{"class":438},[324,739,740,741,743,744,747,748,750],{},"Note the rule covers text, not borders or icons. ",[328,742,375],{},"—input borders, focus rings, icon glyphs—is ",[333,745,746],{},"not"," evaluated by ",[328,749,330],{},"; axe has only partial automated coverage there, so those almost always need manual checking.",[378,752],{},[343,754,756],{"id":755},"why-overlays-gradients-and-images-return-incomplete","Why Overlays, Gradients, and Images Return Incomplete",[324,758,759,760,763,764,766],{},"axe can only compute a ratio when it can determine a single, solid background color. It returns ",[328,761,762],{},"undefined","—classified as ",[333,765,335],{},"—whenever the background is ambiguous:",[348,768,769,775,781,794],{},[351,770,771,774],{},[333,772,773],{},"Background images."," axe can't sample pixels behind the text, so it can't know the local luminance under each glyph.",[351,776,777,780],{},[333,778,779],{},"Gradients."," The contrast varies across the text run; there's no single background value to test against.",[351,782,783,786,787,86,790,793],{},[333,784,785],{},"Semi-transparent overlays."," When a foreground or an intermediate layer uses ",[328,788,789],{},"rgba()",[328,791,792],{},"opacity",", the effective composited color depends on everything beneath it, which axe doesn't flatten reliably.",[351,795,796,799],{},[333,797,798],{},"Overlapping\u002Felevated elements."," If another positioned element sits between the text and its background, axe can't be sure which one actually paints behind the text.",[324,801,802,803,807,808,811],{},"This is correct, conservative behaviour: axe refuses to emit a false pass ",[804,805,806],"em",{},"or"," a false fail. The danger is purely procedural—if your reporting only counts ",[328,809,810],{},"violations",", these nodes vanish. Always surface incompletes:",[419,813,815],{"className":421,"code":814,"language":423,"meta":424,"style":424},"\u002F\u002F A reporting helper that never lets contrast incompletes disappear.\nfunction summariseContrast(results: import('axe-core').AxeResults) {\n  return {\n    failed: results.violations.filter((v) => v.id === 'color-contrast').length,\n    review: results.incomplete\n      .filter((v) => v.id === 'color-contrast')\n      .flatMap((v) => v.nodes.map((n) => n.target.join(' '))),\n  };\n}\n\u002F\u002F review[] is your manual checklist: every one of these is a hero banner,\n\u002F\u002F gradient CTA, or image overlay a human must verify.\n",[328,816,817,822,854,861,889,894,918,962,967,972,977],{"__ignoreMap":424},[428,818,819],{"class":430,"line":431},[428,820,821],{"class":523},"\u002F\u002F A reporting helper that never lets contrast incompletes disappear.\n",[428,823,824,827,830,832,835,838,841,843,846,848,851],{"class":430,"line":452},[428,825,826],{"class":434},"function",[428,828,829],{"class":478}," summariseContrast",[428,831,613],{"class":438},[428,833,834],{"class":546},"results",[428,836,837],{"class":434},":",[428,839,840],{"class":434}," import",[428,842,613],{"class":438},[428,844,845],{"class":445},"'axe-core'",[428,847,400],{"class":438},[428,849,850],{"class":478},"AxeResults",[428,852,853],{"class":438},") {\n",[428,855,856,859],{"class":430,"line":459},[428,857,858],{"class":434},"  return",[428,860,671],{"class":438},[428,862,863,866,868,870,872,874,876,878,880,882,884,886],{"class":430,"line":485},[428,864,865],{"class":438},"    failed: results.violations.",[428,867,540],{"class":478},[428,869,543],{"class":438},[428,871,547],{"class":546},[428,873,550],{"class":438},[428,875,553],{"class":434},[428,877,556],{"class":438},[428,879,559],{"class":434},[428,881,562],{"class":445},[428,883,400],{"class":438},[428,885,622],{"class":465},[428,887,888],{"class":438},",\n",[428,890,891],{"class":430,"line":509},[428,892,893],{"class":438},"    review: results.incomplete\n",[428,895,896,899,901,903,905,907,909,911,913,915],{"class":430,"line":515},[428,897,898],{"class":438},"      .",[428,900,540],{"class":478},[428,902,543],{"class":438},[428,904,547],{"class":546},[428,906,550],{"class":438},[428,908,553],{"class":434},[428,910,556],{"class":438},[428,912,559],{"class":434},[428,914,562],{"class":445},[428,916,917],{"class":438},")\n",[428,919,920,922,925,927,929,931,933,936,939,941,944,946,948,951,954,956,959],{"class":430,"line":520},[428,921,898],{"class":438},[428,923,924],{"class":478},"flatMap",[428,926,543],{"class":438},[428,928,547],{"class":546},[428,930,550],{"class":438},[428,932,553],{"class":434},[428,934,935],{"class":438}," v.nodes.",[428,937,938],{"class":478},"map",[428,940,543],{"class":438},[428,942,943],{"class":546},"n",[428,945,550],{"class":438},[428,947,553],{"class":434},[428,949,950],{"class":438}," n.target.",[428,952,953],{"class":478},"join",[428,955,613],{"class":438},[428,957,958],{"class":445},"' '",[428,960,961],{"class":438},"))),\n",[428,963,964],{"class":430,"line":527},[428,965,966],{"class":438},"  };\n",[428,968,969],{"class":430,"line":568},[428,970,971],{"class":438},"}\n",[428,973,974],{"class":430,"line":599},[428,975,976],{"class":523},"\u002F\u002F review[] is your manual checklist: every one of these is a hero banner,\n",[428,978,979],{"class":430,"line":604},[428,980,981],{"class":523},"\u002F\u002F gradient CTA, or image overlay a human must verify.\n",[324,983,984,985,988],{},"For the common case—white text on a hero image—the fix is to guarantee a worst-case background regardless of the image: a solid scrim or a text-protection gradient with enough opacity that the ",[804,986,987],{},"darkest"," text-over-background pairing still clears 4.5:1. Once a solid effective color exists, axe can evaluate it.",[378,990],{},[343,992,994],{"id":993},"fixing-design-token-contrast","Fixing Design-Token Contrast",[324,996,997],{},"Most contrast failures trace back to a small number of token pairings reused everywhere—muted text on tinted surfaces, brand color on white. Fix the token, fix every instance. Resolve pairings against the WCAG luminance algorithm at the source:",[419,999,1001],{"className":421,"code":1000,"language":423,"meta":424,"style":424},"\u002F\u002F contrast.ts — same algorithm axe and WCAG 2.2 use.\nfunction luminance(hex: string): number {\n  const ch = hex.replace('#', '').match(\u002F.{2}\u002Fg)!.map((c) => {\n    const v = parseInt(c, 16) \u002F 255;\n    return v \u003C= 0.03928 ? v \u002F 12.92 : Math.pow((v + 0.055) \u002F 1.055, 2.4);\n  });\n  return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];\n}\n\nexport function ratio(fg: string, bg: string): number {\n  const [a, b] = [luminance(fg), luminance(bg)];\n  return (Math.max(a, b) + 0.05) \u002F (Math.min(a, b) + 0.05);\n}\n\n\u002F\u002F Guard your design tokens in a unit test so a regression can't merge.\nconst TOKENS = {\n  '--text-muted on --surface': ratio('#6b7280', '#ffffff'),     \u002F\u002F ~4.83 — passes\n  '--brand on --surface':      ratio('#2563eb', '#ffffff'),     \u002F\u002F ~4.55 — passes, barely\n};\n",[328,1002,1003,1008,1035,1101,1128,1184,1188,1235,1239,1243,1280,1312,1347,1352,1357,1363,1375,1403,1428],{"__ignoreMap":424},[428,1004,1005],{"class":430,"line":431},[428,1006,1007],{"class":523},"\u002F\u002F contrast.ts — same algorithm axe and WCAG 2.2 use.\n",[428,1009,1010,1012,1015,1017,1020,1022,1025,1028,1030,1033],{"class":430,"line":452},[428,1011,826],{"class":434},[428,1013,1014],{"class":478}," luminance",[428,1016,613],{"class":438},[428,1018,1019],{"class":546},"hex",[428,1021,837],{"class":434},[428,1023,1024],{"class":465}," string",[428,1026,1027],{"class":438},")",[428,1029,837],{"class":434},[428,1031,1032],{"class":465}," number",[428,1034,671],{"class":438},[428,1036,1037,1040,1043,1045,1048,1051,1053,1056,1058,1061,1063,1066,1068,1070,1073,1076,1078,1081,1083,1086,1088,1090,1092,1095,1097,1099],{"class":430,"line":459},[428,1038,1039],{"class":434},"  const",[428,1041,1042],{"class":465}," ch",[428,1044,469],{"class":434},[428,1046,1047],{"class":438}," hex.",[428,1049,1050],{"class":478},"replace",[428,1052,613],{"class":438},[428,1054,1055],{"class":445},"'#'",[428,1057,500],{"class":438},[428,1059,1060],{"class":445},"''",[428,1062,400],{"class":438},[428,1064,1065],{"class":478},"match",[428,1067,613],{"class":438},[428,1069,86],{"class":445},[428,1071,1072],{"class":465},".",[428,1074,1075],{"class":434},"{2}",[428,1077,86],{"class":445},[428,1079,1080],{"class":434},"g",[428,1082,1027],{"class":438},[428,1084,1085],{"class":434},"!",[428,1087,1072],{"class":438},[428,1089,938],{"class":478},[428,1091,543],{"class":438},[428,1093,1094],{"class":546},"c",[428,1096,550],{"class":438},[428,1098,553],{"class":434},[428,1100,671],{"class":438},[428,1102,1103,1105,1108,1110,1113,1116,1119,1121,1123,1126],{"class":430,"line":485},[428,1104,694],{"class":434},[428,1106,1107],{"class":465}," v",[428,1109,469],{"class":434},[428,1111,1112],{"class":478}," parseInt",[428,1114,1115],{"class":438},"(c, ",[428,1117,1118],{"class":465},"16",[428,1120,550],{"class":438},[428,1122,86],{"class":434},[428,1124,1125],{"class":465}," 255",[428,1127,449],{"class":438},[428,1129,1130,1133,1136,1139,1142,1145,1147,1149,1152,1155,1158,1161,1164,1167,1170,1172,1174,1177,1179,1182],{"class":430,"line":509},[428,1131,1132],{"class":434},"    return",[428,1134,1135],{"class":438}," v ",[428,1137,1138],{"class":434},"\u003C=",[428,1140,1141],{"class":465}," 0.03928",[428,1143,1144],{"class":434}," ?",[428,1146,1135],{"class":438},[428,1148,86],{"class":434},[428,1150,1151],{"class":465}," 12.92",[428,1153,1154],{"class":434}," :",[428,1156,1157],{"class":438}," Math.",[428,1159,1160],{"class":478},"pow",[428,1162,1163],{"class":438},"((v ",[428,1165,1166],{"class":434},"+",[428,1168,1169],{"class":465}," 0.055",[428,1171,550],{"class":438},[428,1173,86],{"class":434},[428,1175,1176],{"class":465}," 1.055",[428,1178,500],{"class":438},[428,1180,1181],{"class":465},"2.4",[428,1183,565],{"class":438},[428,1185,1186],{"class":430,"line":515},[428,1187,733],{"class":438},[428,1189,1190,1192,1195,1198,1201,1203,1206,1208,1211,1213,1215,1218,1220,1222,1225,1227,1229,1232],{"class":430,"line":520},[428,1191,858],{"class":434},[428,1193,1194],{"class":465}," 0.2126",[428,1196,1197],{"class":434}," *",[428,1199,1200],{"class":438}," ch[",[428,1202,705],{"class":465},[428,1204,1205],{"class":438},"] ",[428,1207,1166],{"class":434},[428,1209,1210],{"class":465}," 0.7152",[428,1212,1197],{"class":434},[428,1214,1200],{"class":438},[428,1216,1217],{"class":465},"1",[428,1219,1205],{"class":438},[428,1221,1166],{"class":434},[428,1223,1224],{"class":465}," 0.0722",[428,1226,1197],{"class":434},[428,1228,1200],{"class":438},[428,1230,1231],{"class":465},"2",[428,1233,1234],{"class":438},"];\n",[428,1236,1237],{"class":430,"line":527},[428,1238,971],{"class":438},[428,1240,1241],{"class":430,"line":568},[428,1242,456],{"emptyLinePlaceholder":455},[428,1244,1245,1248,1251,1254,1256,1259,1261,1263,1265,1268,1270,1272,1274,1276,1278],{"class":430,"line":599},[428,1246,1247],{"class":434},"export",[428,1249,1250],{"class":434}," function",[428,1252,1253],{"class":478}," ratio",[428,1255,613],{"class":438},[428,1257,1258],{"class":546},"fg",[428,1260,837],{"class":434},[428,1262,1024],{"class":465},[428,1264,500],{"class":438},[428,1266,1267],{"class":546},"bg",[428,1269,837],{"class":434},[428,1271,1024],{"class":465},[428,1273,1027],{"class":438},[428,1275,837],{"class":434},[428,1277,1032],{"class":465},[428,1279,671],{"class":438},[428,1281,1282,1284,1287,1289,1291,1294,1296,1299,1301,1304,1307,1309],{"class":430,"line":604},[428,1283,1039],{"class":434},[428,1285,1286],{"class":438}," [",[428,1288,338],{"class":465},[428,1290,500],{"class":438},[428,1292,1293],{"class":465},"b",[428,1295,1205],{"class":438},[428,1297,1298],{"class":434},"=",[428,1300,1286],{"class":438},[428,1302,1303],{"class":478},"luminance",[428,1305,1306],{"class":438},"(fg), ",[428,1308,1303],{"class":478},[428,1310,1311],{"class":438},"(bg)];\n",[428,1313,1314,1316,1319,1322,1325,1327,1330,1332,1334,1336,1339,1341,1343,1345],{"class":430,"line":627},[428,1315,858],{"class":434},[428,1317,1318],{"class":438}," (Math.",[428,1320,1321],{"class":478},"max",[428,1323,1324],{"class":438},"(a, b) ",[428,1326,1166],{"class":434},[428,1328,1329],{"class":465}," 0.05",[428,1331,550],{"class":438},[428,1333,86],{"class":434},[428,1335,1318],{"class":438},[428,1337,1338],{"class":478},"min",[428,1340,1324],{"class":438},[428,1342,1166],{"class":434},[428,1344,1329],{"class":465},[428,1346,565],{"class":438},[428,1348,1350],{"class":430,"line":1349},13,[428,1351,971],{"class":438},[428,1353,1355],{"class":430,"line":1354},14,[428,1356,456],{"emptyLinePlaceholder":455},[428,1358,1360],{"class":430,"line":1359},15,[428,1361,1362],{"class":523},"\u002F\u002F Guard your design tokens in a unit test so a regression can't merge.\n",[428,1364,1366,1368,1371,1373],{"class":430,"line":1365},16,[428,1367,462],{"class":434},[428,1369,1370],{"class":465}," TOKENS",[428,1372,469],{"class":434},[428,1374,671],{"class":438},[428,1376,1378,1381,1384,1387,1389,1392,1394,1397,1400],{"class":430,"line":1377},17,[428,1379,1380],{"class":445},"  '--text-muted on --surface'",[428,1382,1383],{"class":438},": ",[428,1385,1386],{"class":478},"ratio",[428,1388,613],{"class":438},[428,1390,1391],{"class":445},"'#6b7280'",[428,1393,500],{"class":438},[428,1395,1396],{"class":445},"'#ffffff'",[428,1398,1399],{"class":438},"),     ",[428,1401,1402],{"class":523},"\u002F\u002F ~4.83 — passes\n",[428,1404,1406,1409,1412,1414,1416,1419,1421,1423,1425],{"class":430,"line":1405},18,[428,1407,1408],{"class":445},"  '--brand on --surface'",[428,1410,1411],{"class":438},":      ",[428,1413,1386],{"class":478},[428,1415,613],{"class":438},[428,1417,1418],{"class":445},"'#2563eb'",[428,1420,500],{"class":438},[428,1422,1396],{"class":445},[428,1424,1399],{"class":438},[428,1426,1427],{"class":523},"\u002F\u002F ~4.55 — passes, barely\n",[428,1429,1431],{"class":430,"line":1430},19,[428,1432,1433],{"class":438},"};\n",[419,1435,1437],{"className":421,"code":1436,"language":423,"meta":424,"style":424},"\u002F\u002F tokens.contrast.test.ts\nimport { ratio } from '.\u002Fcontrast';\n\nit('muted text clears 1.4.3 Contrast (Minimum) on surface', () => {\n  expect(ratio('#6b7280', '#ffffff')).toBeGreaterThanOrEqual(4.5);\n});\n",[328,1438,1439,1444,1458,1462,1479,1509],{"__ignoreMap":424},[428,1440,1441],{"class":430,"line":431},[428,1442,1443],{"class":523},"\u002F\u002F tokens.contrast.test.ts\n",[428,1445,1446,1448,1451,1453,1456],{"class":430,"line":452},[428,1447,435],{"class":434},[428,1449,1450],{"class":438}," { ratio } ",[428,1452,442],{"class":434},[428,1454,1455],{"class":445}," '.\u002Fcontrast'",[428,1457,449],{"class":438},[428,1459,1460],{"class":430,"line":459},[428,1461,456],{"emptyLinePlaceholder":455},[428,1463,1464,1467,1469,1472,1475,1477],{"class":430,"line":485},[428,1465,1466],{"class":478},"it",[428,1468,613],{"class":438},[428,1470,1471],{"class":445},"'muted text clears 1.4.3 Contrast (Minimum) on surface'",[428,1473,1474],{"class":438},", () ",[428,1476,553],{"class":434},[428,1478,671],{"class":438},[428,1480,1481,1484,1486,1488,1490,1492,1494,1496,1499,1502,1504,1507],{"class":430,"line":509},[428,1482,1483],{"class":478},"  expect",[428,1485,613],{"class":438},[428,1487,1386],{"class":478},[428,1489,613],{"class":438},[428,1491,1391],{"class":445},[428,1493,500],{"class":438},[428,1495,1396],{"class":445},[428,1497,1498],{"class":438},")).",[428,1500,1501],{"class":478},"toBeGreaterThanOrEqual",[428,1503,613],{"class":438},[428,1505,1506],{"class":465},"4.5",[428,1508,565],{"class":438},[428,1510,1511],{"class":430,"line":515},[428,1512,512],{"class":438},[324,1514,1515],{},"Two token-level rules that prevent most regressions:",[348,1517,1518,1526],{},[351,1519,1520,1525],{},[333,1521,1522,1523,1072],{},"Never express a contrast difference with ",[328,1524,792],{}," Lowering a token's alpha changes the composited luminance unpredictably and fails the WCAG calculation. Define an explicit darker\u002Flighter token instead.",[351,1527,1528,1531,1532,1535],{},[333,1529,1530],{},"Test tokens against every surface they render on",", including the dark theme, not just the light default. A pair that passes on ",[328,1533,1534],{},"#ffffff"," can fail on a tinted card.",[324,1537,1538,1539,1072],{},"For the full token and theming architecture this slots into, see ",[338,1540,364],{"href":363},[378,1542],{},[343,1544,1546],{"id":1545},"how-to-verify","How to Verify",[324,1548,1549],{},"Contrast is verifiable both programmatically and manually—do both, because each catches what the other misses.",[324,1551,1552],{},[333,1553,1554],{},"Programmatic (catches solid-background failures):",[419,1556,1558],{"className":421,"code":1557,"language":423,"meta":424,"style":424},"\u002F\u002F In jest-axe or Playwright, assert zero contrast violations AND zero\n\u002F\u002F unaddressed incompletes for the components under test.\nconst { violations, incomplete } = await axe.run(container, {\n  runOnly: { rules: ['color-contrast'] },\n});\nexpect(violations).toEqual([]);\n\u002F\u002F Fail the build if a contrast incomplete appears on a component that\n\u002F\u002F shouldn't have one — it means an overlay\u002Fgradient slipped in unreviewed.\nexpect(incomplete.filter((i) => i.id === 'color-contrast')).toEqual([]);\n",[328,1559,1560,1565,1570,1597,1607,1611,1625,1630,1635],{"__ignoreMap":424},[428,1561,1562],{"class":430,"line":431},[428,1563,1564],{"class":523},"\u002F\u002F In jest-axe or Playwright, assert zero contrast violations AND zero\n",[428,1566,1567],{"class":430,"line":452},[428,1568,1569],{"class":523},"\u002F\u002F unaddressed incompletes for the components under test.\n",[428,1571,1572,1574,1577,1579,1581,1583,1586,1588,1590,1592,1594],{"class":430,"line":459},[428,1573,462],{"class":434},[428,1575,1576],{"class":438}," { ",[428,1578,810],{"class":465},[428,1580,500],{"class":438},[428,1582,335],{"class":465},[428,1584,1585],{"class":438}," } ",[428,1587,1298],{"class":434},[428,1589,472],{"class":434},[428,1591,475],{"class":438},[428,1593,479],{"class":478},[428,1595,1596],{"class":438},"(container, {\n",[428,1598,1599,1602,1605],{"class":430,"line":485},[428,1600,1601],{"class":438},"  runOnly: { rules: [",[428,1603,1604],{"class":445},"'color-contrast'",[428,1606,506],{"class":438},[428,1608,1609],{"class":430,"line":509},[428,1610,512],{"class":438},[428,1612,1613,1616,1619,1622],{"class":430,"line":515},[428,1614,1615],{"class":478},"expect",[428,1617,1618],{"class":438},"(violations).",[428,1620,1621],{"class":478},"toEqual",[428,1623,1624],{"class":438},"([]);\n",[428,1626,1627],{"class":430,"line":520},[428,1628,1629],{"class":523},"\u002F\u002F Fail the build if a contrast incomplete appears on a component that\n",[428,1631,1632],{"class":430,"line":527},[428,1633,1634],{"class":523},"\u002F\u002F shouldn't have one — it means an overlay\u002Fgradient slipped in unreviewed.\n",[428,1636,1637,1639,1642,1644,1646,1649,1651,1653,1656,1658,1660,1662,1664],{"class":430,"line":568},[428,1638,1615],{"class":478},[428,1640,1641],{"class":438},"(incomplete.",[428,1643,540],{"class":478},[428,1645,543],{"class":438},[428,1647,1648],{"class":546},"i",[428,1650,550],{"class":438},[428,1652,553],{"class":434},[428,1654,1655],{"class":438}," i.id ",[428,1657,559],{"class":434},[428,1659,562],{"class":445},[428,1661,1498],{"class":438},[428,1663,1621],{"class":478},[428,1665,1624],{"class":438},[324,1667,1668],{},[333,1669,1670],{},"Manual (catches everything axe marked incomplete):",[348,1672,1673,1680,1687],{},[351,1674,1675,1676,1679],{},"Use the axe DevTools or browser eyedropper to sample the ",[804,1677,1678],{},"actual"," rendered background under text on hero images, gradients, and overlays at the worst-case pixel. Compute the ratio against that sample.",[351,1681,1682,1683,1686],{},"Toggle the OS into dark mode and high-contrast\u002F",[328,1684,1685],{},"forced-colors"," mode and re-check the same surfaces—token pairings that pass in light can fail elsewhere.",[351,1688,1689,1690,1692,1693,1696],{},"For ",[328,1691,375],{},", manually verify input borders, icon contrast, and the ",[328,1694,1695],{},":focus-visible"," ring clear 3:1 against their adjacent colors; axe won't reliably do this for you.",[324,1698,1699,1700,1703],{},"A component is clear only when programmatic violations are zero ",[804,1701,1702],{},"and"," every contrast incomplete has been sampled and confirmed by hand.",[378,1705],{},[343,1707,1709],{"id":1708},"common-a11y-mistakes","Common a11y Mistakes",[1711,1712,1713,1729,1738,1744,1756],"ol",{},[351,1714,1715,1718,1719,1721,1722,1725,1726,1728],{},[333,1716,1717],{},"Counting incompletes as passes."," A hero with white text on a photo reports ",[328,1720,335],{},", not ",[328,1723,1724],{},"pass",". If your gate only checks ",[328,1727,810],{},", it ships unverified. Surface and review every contrast incomplete.",[351,1730,1731,1737],{},[333,1732,1733,1734,1736],{},"Using ",[328,1735,792],{}," for \"muted\" text."," It changes composited luminance and fails WCAG math. Use an explicit color token.",[351,1739,1740,1743],{},[333,1741,1742],{},"Testing tokens against the light theme only."," Re-run for dark and high-contrast themes; the same pairing can flip from pass to fail.",[351,1745,1746,1749,1750,1752,1753,1755],{},[333,1747,1748],{},"Forgetting non-text contrast."," ",[328,1751,330],{}," ignores borders, icons, and focus rings. ",[328,1754,375],{}," needs manual verification.",[351,1757,1758,1761],{},[333,1759,1760],{},"Fixing the instance, not the token."," Patching one component's color leaves the same failing pairing live everywhere else it's used. Fix at the token source and test it.",[378,1763],{},[343,1765,1767],{"id":1766},"conclusion","Conclusion",[324,1769,1770,1771,1773],{},"axe-core's ",[328,1772,330],{}," rule is a reliable gate for solid-background text and a precise pointer to ambiguous cases via its incomplete results. The discipline that makes it effective is refusing to treat \"needs review\" as \"passed\": every gradient, overlay, and image-backed text run is a manual checkpoint. Fix contrast at the design-token layer, guard those tokens with unit assertions, and verify the rest with an eyedropper and dark\u002Fhigh-contrast modes—then your automated run and your real users agree.",[378,1775],{},[343,1777,1779],{"id":1778},"frequently-asked-questions","Frequently Asked Questions",[324,1781,1782,1785,1786,1788,1789,1791],{},[333,1783,1784],{},"Why did axe mark my hero text as \"incomplete\" instead of failing it?","\nBecause the text sits over a background axe can't reduce to a single solid color—an image, gradient, or semi-transparent overlay. It returns ",[328,1787,762],{}," rather than guess, which is classified as incomplete. Sample the worst-case background pixel manually and compute the ratio against ",[328,1790,371],{},"; add a solid scrim if it falls short.",[324,1793,1794,1797,1798,1800,1801,1803],{},[333,1795,1796],{},"Does the color-contrast rule check icons, borders, and focus rings?","\nNo. The ",[328,1799,330],{}," rule only evaluates text. ",[328,1802,375],{},"—UI component borders, icon glyphs, and focus indicators at 3:1—has only partial automated coverage in axe, so verify those by hand with an eyedropper.",[324,1805,1806,1809,1810,1812,1813,1815,1816,1818],{},[333,1807,1808],{},"How do I fail a build on contrast issues but still allow reviewed exceptions?","\nAssert ",[328,1811,810],{}," for ",[328,1814,330],{}," is empty, and assert that contrast ",[328,1817,335],{}," entries only appear on components you've explicitly reviewed (record those as documented exceptions). That way a new unreviewed gradient or overlay still breaks the build.",[324,1820,1821,1824,1826],{},[333,1822,1823],{},"Why does using opacity for muted text fail contrast checks?",[328,1825,792],{}," and alpha channels composite the text against whatever is behind it, changing the effective luminance unpredictably. The WCAG ratio is computed from solid colors, so axe (and real assistive-tech users) see a different value than you intended. Define an explicit darker token instead.",[378,1828],{},[343,1830,1832],{"id":1831},"related-guides","Related guides",[348,1834,1835,1840,1845],{},[351,1836,1837,1839],{},[338,1838,243],{"href":340}," — the guide on the rules engine.",[351,1841,1842,1844],{},[338,1843,13],{"href":363}," — the token architecture these fixes build on.",[351,1846,1847,1850],{},[338,1848,255],{"href":1849},"\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules\u002F"," — encode project-specific contrast checks into the engine.",[1852,1853,1854],"style",{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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":424,"searchDepth":452,"depth":452,"links":1856},[1857,1858,1859,1860,1861,1862,1863,1864,1865],{"id":345,"depth":452,"text":346},{"id":382,"depth":452,"text":383},{"id":755,"depth":452,"text":756},{"id":993,"depth":452,"text":994},{"id":1545,"depth":452,"text":1546},{"id":1708,"depth":452,"text":1709},{"id":1766,"depth":452,"text":1767},{"id":1778,"depth":452,"text":1779},{"id":1831,"depth":452,"text":1832},null,"Catch low-contrast text and UI before it ships—how axe-core's color-contrast rule works, why it flags gradients and overlays as incomplete, and how to fix tokens.","md",{},false,{"title":249,"description":1867},"gafiTTMaTIr-j7rC8W3EQDYQxysRyLlm1gufNyU3SuU",[1874,1913,1914,1977],{"title":5,"path":6,"stem":7,"children":1875},[1876,1877,1880,1883,1889,1895,1904,1910],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":1878},[1879],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":1881},[1882],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":1884},[1885,1886],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":1887},[1888],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":1890},[1891,1892],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":1893},[1894],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":1896},[1897,1898,1901],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":1899},[1900],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":1902},[1903],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":1905},[1906,1907],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":1908},[1909],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":1911},[1912],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":1915},[1916,1917,1923,1935,1947,1950,1959,1971],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":1918},[1919,1920],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":1921},[1922],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":1924},[1925,1926,1929,1932],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":1927},[1928],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":1930},[1931],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":1933},[1934],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":1936},[1937,1938,1941,1944],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":1939},[1940],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":1942},[1943],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":1945},[1946],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":1948},[1949],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":1951},[1952,1953,1956],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":1954},[1955],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":1957},[1958],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":1960},[1961,1962,1965,1968],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":1963},[1964],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":1966},[1967],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":1969},[1970],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":1972},[1973,1974],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":1975},[1976],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":1978},[1979,1980,1989,1998,2007,2016],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":1981},[1982,1983,1986],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":1984},[1985],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":1987},[1988],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":1990},[1991,1992,1995],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":1993},[1994],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":1996},[1997],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":1999},[2000,2001,2004],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2002},[2003],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2005},[2006],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2008},[2009,2010,2013],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2011},[2012],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2014},[2015],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2017},[2018,2019,2022],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2020},[2021],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2023},[2024],{"title":309,"path":310,"stem":311},1781785524230]