[{"data":1,"prerenderedAt":2399},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002F":156,"content-navigation":2325},[4,66,70],{"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,60],{"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 in Modern Frameworks","\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},"Screen Reader Compatibility Testing for Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Findex",[53,54],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"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",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"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",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},"Modern Framework Accessibility","\u002F","index",{"title":71,"path":72,"stem":73,"children":74},"React Nextjs Accessibility Patterns","\u002Freact-nextjs-accessibility-patterns","react-nextjs-accessibility-patterns",[75,78,90,102,108,126,144],{"title":76,"path":72,"stem":77},"React & Next.js Accessibility Patterns","react-nextjs-accessibility-patterns\u002Findex",{"title":79,"path":80,"stem":81,"children":82},"Accessible Component Libraries in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Findex",[83,84],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"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",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Dynamic Content & State Announcements in React & Next.js","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Findex",[95,96],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"Implementing React Context for Global 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",[101],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Form Handling with React Hook Form & Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y","react-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"Next.js App Router & A11y: Implementation Guide for Modern Frameworks","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Findex",[113,114,120],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Implementing Skip Links in Next.js App Router: A Step-by-Step Guide","\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",[119],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Next.js Dynamic Imports and Keyboard Navigation: A Complete A11y Implementation Guide","\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",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"React Hooks for Accessibility: Implementation Patterns & State Management","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Findex",[131,132,138],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"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",[137],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"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",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Server Components & Client-Side Interactivity","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex",[149,150],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"Handling 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",[155],{"title":151,"path":152,"stem":153},{"id":157,"title":13,"body":158,"date":2318,"description":2319,"extension":2320,"image":2318,"meta":2321,"modifiedAt":2318,"navigation":423,"noindex":2322,"path":14,"publishedAt":2318,"seo":2323,"stem":15,"updatedAt":2318,"__hash__":2324},"content\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002Findex.md",{"type":159,"value":160,"toc":2308},"minimark",[161,165,174,179,208,212,223,226,231,234,239,270,737,747,749,753,756,761,808,1443,1458,1470,1472,1476,1479,1484,1505,1938,1947,1949,1953,1956,1960,1982,2170,2191,2204,2206,2210,2260,2262,2266,2272,2289,2295,2305],[162,163,13],"h1",{"id":164},"accessible-color-contrast-theming",[166,167,168,169,173],"p",{},"Implementing accessible color contrast and dynamic theming requires more than static design tokens; it demands runtime validation, CSS variable architecture, and framework-aware state management to maintain compliance across light, dark, and high-contrast modes. Building on ",[170,171,10],"a",{"href":172},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002F",", this guide bridges foundational WCAG requirements with practical implementation patterns for component-driven architectures.",[175,176,178],"h3",{"id":177},"target-wcag-22-criteria","Target WCAG 2.2 Criteria",[180,181,182,190,196,202],"ul",{},[183,184,185,189],"li",{},[186,187,188],"strong",{},"1.4.3 (Contrast Minimum):"," 4.5:1 for normal text, 3:1 for large text (18pt\u002F14pt bold)",[183,191,192,195],{},[186,193,194],{},"1.4.6 (Contrast Enhanced):"," 7:1 for normal text, 4.5:1 for large text",[183,197,198,201],{},[186,199,200],{},"1.4.11 (Non-text Contrast):"," 3:1 for UI components, graphical objects, and focus indicators",[183,203,204,207],{},[186,205,206],{},"1.4.10 (Reflow):"," Content must remain readable and operable without horizontal scrolling at 400% zoom, which directly impacts token scaling strategies",[175,209,211],{"id":210},"core-implementation-principles","Core Implementation Principles",[180,213,214,217,220],{},[183,215,216],{},"Dynamic theme switching must preserve minimum 4.5:1 contrast for text and 3:1 for UI components",[183,218,219],{},"CSS custom properties enable runtime contrast validation without JavaScript overhead",[183,221,222],{},"Framework state should drive theme context without triggering layout thrashing or hydration mismatches",[224,225],"hr",{},[227,228,230],"h2",{"id":229},"css-custom-properties-token-architecture","CSS Custom Properties & Token Architecture",[166,232,233],{},"A scalable, framework-agnostic color system relies on semantic token mapping rather than hardcoded hex values. By decoupling design intent from implementation, you enforce contrast ratios at the stylesheet level while allowing frameworks to consume predictable, validated values.",[166,235,236],{},[186,237,238],{},"Implementation Guidelines:",[180,240,241,256,267],{},[183,242,243,244,248,249,248,252,255],{},"Define semantic variables (",[245,246,247],"code",{},"--color-text-primary",", ",[245,250,251],{},"--color-surface",[245,253,254],{},"--color-border-focus",") instead of raw color names or hex codes.",[183,257,258,259,262,263,266],{},"Use ",[245,260,261],{},"clamp()"," and ",[245,264,265],{},"calc()"," for responsive contrast scaling that adapts to zoom levels and viewport constraints.",[183,268,269],{},"Map design tokens to WCAG-compliant palettes during build time using tools like Style Dictionary or Figma Tokens.",[271,272,277],"pre",{"className":273,"code":274,"language":275,"meta":276,"style":276},"language-css shiki shiki-themes github-light github-dark","\u002F* design-tokens.css *\u002F\n:root {\n \u002F* Base semantic tokens *\u002F\n --color-surface: #ffffff;\n --color-text-primary: #1a1a1a;\n --color-text-secondary: #4a4a4a;\n --color-border: #d4d4d4;\n --color-focus-ring: #005fcc;\n \n \u002F* Non-text contrast baseline (3:1 minimum) *\u002F\n --color-icon-default: #555555;\n --color-input-border: #888888;\n}\n\n[data-theme=\"dark\"] {\n --color-surface: #0f0f0f;\n --color-text-primary: #f5f5f5;\n --color-text-secondary: #b3b3b3;\n --color-border: #333333;\n --color-focus-ring: #60a5fa;\n --color-icon-default: #a1a1aa;\n --color-input-border: #71717a;\n}\n\n\u002F* High-contrast system override *\u002F\n@media (prefers-contrast: more) {\n :root, [data-theme=\"dark\"] {\n --color-text-primary: CanvasText;\n --color-surface: Canvas;\n --color-border: ButtonBorder;\n --color-focus-ring: Highlight;\n }\n}\n\n\u002F* Component consumption *\u002F\n.card {\n background-color: var(--color-surface);\n color: var(--color-text-primary);\n border: 1px solid var(--color-border);\n transition: background-color 0.2s ease, color 0.2s ease;\n}\n","css","",[245,278,279,288,299,305,322,335,348,361,374,380,386,399,412,418,425,445,457,469,481,493,505,517,529,534,539,545,554,571,579,587,595,608,614,619,624,630,638,657,673,700,732],{"__ignoreMap":276},[280,281,284],"span",{"class":282,"line":283},"line",1,[280,285,287],{"class":286},"sJ8bj","\u002F* design-tokens.css *\u002F\n",[280,289,291,295],{"class":282,"line":290},2,[280,292,294],{"class":293},"sScJk",":root",[280,296,298],{"class":297},"sVt8B"," {\n",[280,300,302],{"class":282,"line":301},3,[280,303,304],{"class":286}," \u002F* Base semantic tokens *\u002F\n",[280,306,308,312,315,319],{"class":282,"line":307},4,[280,309,311],{"class":310},"s4XuR"," --color-surface",[280,313,314],{"class":297},": ",[280,316,318],{"class":317},"sj4cs","#ffffff",[280,320,321],{"class":297},";\n",[280,323,325,328,330,333],{"class":282,"line":324},5,[280,326,327],{"class":310}," --color-text-primary",[280,329,314],{"class":297},[280,331,332],{"class":317},"#1a1a1a",[280,334,321],{"class":297},[280,336,338,341,343,346],{"class":282,"line":337},6,[280,339,340],{"class":310}," --color-text-secondary",[280,342,314],{"class":297},[280,344,345],{"class":317},"#4a4a4a",[280,347,321],{"class":297},[280,349,351,354,356,359],{"class":282,"line":350},7,[280,352,353],{"class":310}," --color-border",[280,355,314],{"class":297},[280,357,358],{"class":317},"#d4d4d4",[280,360,321],{"class":297},[280,362,364,367,369,372],{"class":282,"line":363},8,[280,365,366],{"class":310}," --color-focus-ring",[280,368,314],{"class":297},[280,370,371],{"class":317},"#005fcc",[280,373,321],{"class":297},[280,375,377],{"class":282,"line":376},9,[280,378,379],{"class":297}," \n",[280,381,383],{"class":282,"line":382},10,[280,384,385],{"class":286}," \u002F* Non-text contrast baseline (3:1 minimum) *\u002F\n",[280,387,389,392,394,397],{"class":282,"line":388},11,[280,390,391],{"class":310}," --color-icon-default",[280,393,314],{"class":297},[280,395,396],{"class":317},"#555555",[280,398,321],{"class":297},[280,400,402,405,407,410],{"class":282,"line":401},12,[280,403,404],{"class":310}," --color-input-border",[280,406,314],{"class":297},[280,408,409],{"class":317},"#888888",[280,411,321],{"class":297},[280,413,415],{"class":282,"line":414},13,[280,416,417],{"class":297},"}\n",[280,419,421],{"class":282,"line":420},14,[280,422,424],{"emptyLinePlaceholder":423},true,"\n",[280,426,428,431,434,438,442],{"class":282,"line":427},15,[280,429,430],{"class":297},"[",[280,432,433],{"class":293},"data-theme",[280,435,437],{"class":436},"szBVR","=",[280,439,441],{"class":440},"sZZnC","\"dark\"",[280,443,444],{"class":297},"] {\n",[280,446,448,450,452,455],{"class":282,"line":447},16,[280,449,311],{"class":310},[280,451,314],{"class":297},[280,453,454],{"class":317},"#0f0f0f",[280,456,321],{"class":297},[280,458,460,462,464,467],{"class":282,"line":459},17,[280,461,327],{"class":310},[280,463,314],{"class":297},[280,465,466],{"class":317},"#f5f5f5",[280,468,321],{"class":297},[280,470,472,474,476,479],{"class":282,"line":471},18,[280,473,340],{"class":310},[280,475,314],{"class":297},[280,477,478],{"class":317},"#b3b3b3",[280,480,321],{"class":297},[280,482,484,486,488,491],{"class":282,"line":483},19,[280,485,353],{"class":310},[280,487,314],{"class":297},[280,489,490],{"class":317},"#333333",[280,492,321],{"class":297},[280,494,496,498,500,503],{"class":282,"line":495},20,[280,497,366],{"class":310},[280,499,314],{"class":297},[280,501,502],{"class":317},"#60a5fa",[280,504,321],{"class":297},[280,506,508,510,512,515],{"class":282,"line":507},21,[280,509,391],{"class":310},[280,511,314],{"class":297},[280,513,514],{"class":317},"#a1a1aa",[280,516,321],{"class":297},[280,518,520,522,524,527],{"class":282,"line":519},22,[280,521,404],{"class":310},[280,523,314],{"class":297},[280,525,526],{"class":317},"#71717a",[280,528,321],{"class":297},[280,530,532],{"class":282,"line":531},23,[280,533,417],{"class":297},[280,535,537],{"class":282,"line":536},24,[280,538,424],{"emptyLinePlaceholder":423},[280,540,542],{"class":282,"line":541},25,[280,543,544],{"class":286},"\u002F* High-contrast system override *\u002F\n",[280,546,548,551],{"class":282,"line":547},26,[280,549,550],{"class":436},"@media",[280,552,553],{"class":297}," (prefers-contrast: more) {\n",[280,555,557,560,563,565,567,569],{"class":282,"line":556},27,[280,558,559],{"class":293}," :root",[280,561,562],{"class":297},", [",[280,564,433],{"class":293},[280,566,437],{"class":436},[280,568,441],{"class":440},[280,570,444],{"class":297},[280,572,574,576],{"class":282,"line":573},28,[280,575,327],{"class":310},[280,577,578],{"class":297},": CanvasText;\n",[280,580,582,584],{"class":282,"line":581},29,[280,583,311],{"class":310},[280,585,586],{"class":297},": Canvas;\n",[280,588,590,592],{"class":282,"line":589},30,[280,591,353],{"class":310},[280,593,594],{"class":297},": ButtonBorder;\n",[280,596,598,600,602,606],{"class":282,"line":597},31,[280,599,366],{"class":310},[280,601,314],{"class":297},[280,603,605],{"class":604},"s7hpK","Highlight",[280,607,321],{"class":297},[280,609,611],{"class":282,"line":610},32,[280,612,613],{"class":297}," }\n",[280,615,617],{"class":282,"line":616},33,[280,618,417],{"class":297},[280,620,622],{"class":282,"line":621},34,[280,623,424],{"emptyLinePlaceholder":423},[280,625,627],{"class":282,"line":626},35,[280,628,629],{"class":286},"\u002F* Component consumption *\u002F\n",[280,631,633,636],{"class":282,"line":632},36,[280,634,635],{"class":293},".card",[280,637,298],{"class":297},[280,639,641,644,646,649,652,654],{"class":282,"line":640},37,[280,642,643],{"class":317}," background-color",[280,645,314],{"class":297},[280,647,648],{"class":317},"var",[280,650,651],{"class":297},"(",[280,653,251],{"class":310},[280,655,656],{"class":297},");\n",[280,658,660,663,665,667,669,671],{"class":282,"line":659},38,[280,661,662],{"class":317}," color",[280,664,314],{"class":297},[280,666,648],{"class":317},[280,668,651],{"class":297},[280,670,247],{"class":310},[280,672,656],{"class":297},[280,674,676,679,681,684,687,690,693,695,698],{"class":282,"line":675},39,[280,677,678],{"class":317}," border",[280,680,314],{"class":297},[280,682,683],{"class":317},"1",[280,685,686],{"class":436},"px",[280,688,689],{"class":317}," solid",[280,691,692],{"class":317}," var",[280,694,651],{"class":297},[280,696,697],{"class":310},"--color-border",[280,699,656],{"class":297},[280,701,703,706,709,712,715,718,720,723,726,728,730],{"class":282,"line":702},40,[280,704,705],{"class":317}," transition",[280,707,708],{"class":297},": background-color ",[280,710,711],{"class":317},"0.2",[280,713,714],{"class":436},"s",[280,716,717],{"class":317}," ease",[280,719,248],{"class":297},[280,721,722],{"class":317},"color",[280,724,725],{"class":317}," 0.2",[280,727,714],{"class":436},[280,729,717],{"class":317},[280,731,321],{"class":297},[280,733,735],{"class":282,"line":734},41,[280,736,417],{"class":297},[166,738,739,742,743,746],{},[186,740,741],{},"Testing Hook:"," Validate token mappings against automated contrast checkers (e.g., axe-core, pa11y) before deployment to staging. Use ",[245,744,745],{},"data-testid=\"theme-token-surface\""," on root containers to programmatically assert computed styles in E2E tests.",[224,748],{},[227,750,752],{"id":751},"framework-state-management-for-theme-context","Framework State Management for Theme Context",[166,754,755],{},"Synchronizing theme state across component trees requires careful handling of reactivity, hydration, and DOM inheritance. Theme toggles must be exposed to assistive technologies, and state updates must avoid breaking token inheritance chains.",[166,757,758],{},[186,759,760],{},"Framework Constraints & Best Practices:",[180,762,763,785,798],{},[183,764,765,768,769,772,773,776,777,780,781,784],{},[186,766,767],{},"React:"," Avoid inline ",[245,770,771],{},"style"," overrides that bypass CSS cascade. Use ",[245,774,775],{},"useSyncExternalStore"," or Context with ",[245,778,779],{},"useEffect"," to prevent hydration mismatches when reading ",[245,782,783],{},"prefers-color-scheme",".",[183,786,787,790,791,68,794,797],{},[186,788,789],{},"Vue\u002FAngular:"," Leverage ",[245,792,793],{},"provide",[245,795,796],{},"inject"," or services to propagate theme state. Ensure reactivity systems don't trigger unnecessary repaints by batching DOM attribute updates.",[183,799,800,803,804,807],{},[186,801,802],{},"Portals:"," Content rendered outside the main DOM tree (modals, dropdowns) inherits CSS variables from the ",[245,805,806],{},"document.documentElement",". Explicitly scope portal containers if they require isolated theming.",[271,809,813],{"className":810,"code":811,"language":812,"meta":276,"style":276},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F ThemeProvider.tsx (React)\nimport { createContext, useContext, useEffect, useState } from 'react';\n\ntype Theme = 'light' | 'dark';\n\ninterface ThemeContextValue {\n theme: Theme;\n toggleTheme: () => void;\n}\n\nconst ThemeContext = createContext\u003CThemeContextValue | null>(null);\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n \u002F\u002F Initialize with system preference to prevent hydration mismatch\n const [theme, setTheme] = useState\u003CTheme>(() => {\n if (typeof window !== 'undefined') {\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n }\n return 'light';\n });\n\n useEffect(() => {\n const root = document.documentElement;\n root.setAttribute('data-theme', theme);\n \n \u002F\u002F Announce theme change to screen readers\n const statusEl = document.getElementById('a11y-theme-status');\n if (statusEl) {\n statusEl.textContent = `Theme switched to ${theme} mode`;\n }\n }, [theme]);\n\n const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');\n\n return (\n \u003CThemeContext.Provider value={{ theme, toggleTheme }}>\n {\u002F* Live region for AT announcements *\u002F}\n \u003Cdiv id=\"a11y-theme-status\" role=\"status\" aria-live=\"polite\" className=\"sr-only\" \u002F>\n {children}\n \u003C\u002FThemeContext.Provider>\n );\n}\n\nexport const useTheme = () => {\n const context = useContext(ThemeContext);\n if (!context) throw new Error('useTheme must be used within ThemeProvider');\n return context;\n};\n","tsx",[245,814,815,820,836,840,862,866,876,888,906,910,914,946,950,990,995,1031,1054,1085,1089,1097,1102,1106,1118,1130,1146,1150,1155,1177,1184,1201,1205,1210,1214,1256,1260,1267,1283,1293,1336,1341,1351,1356,1361,1366,1384,1400,1429,1437],{"__ignoreMap":276},[280,816,817],{"class":282,"line":283},[280,818,819],{"class":286},"\u002F\u002F ThemeProvider.tsx (React)\n",[280,821,822,825,828,831,834],{"class":282,"line":290},[280,823,824],{"class":436},"import",[280,826,827],{"class":297}," { createContext, useContext, useEffect, useState } ",[280,829,830],{"class":436},"from",[280,832,833],{"class":440}," 'react'",[280,835,321],{"class":297},[280,837,838],{"class":282,"line":301},[280,839,424],{"emptyLinePlaceholder":423},[280,841,842,845,848,851,854,857,860],{"class":282,"line":307},[280,843,844],{"class":436},"type",[280,846,847],{"class":293}," Theme",[280,849,850],{"class":436}," =",[280,852,853],{"class":440}," 'light'",[280,855,856],{"class":436}," |",[280,858,859],{"class":440}," 'dark'",[280,861,321],{"class":297},[280,863,864],{"class":282,"line":324},[280,865,424],{"emptyLinePlaceholder":423},[280,867,868,871,874],{"class":282,"line":337},[280,869,870],{"class":436},"interface",[280,872,873],{"class":293}," ThemeContextValue",[280,875,298],{"class":297},[280,877,878,881,884,886],{"class":282,"line":350},[280,879,880],{"class":310}," theme",[280,882,883],{"class":436},":",[280,885,847],{"class":293},[280,887,321],{"class":297},[280,889,890,893,895,898,901,904],{"class":282,"line":363},[280,891,892],{"class":293}," toggleTheme",[280,894,883],{"class":436},[280,896,897],{"class":297}," () ",[280,899,900],{"class":436},"=>",[280,902,903],{"class":317}," void",[280,905,321],{"class":297},[280,907,908],{"class":282,"line":376},[280,909,417],{"class":297},[280,911,912],{"class":282,"line":382},[280,913,424],{"emptyLinePlaceholder":423},[280,915,916,919,922,924,927,930,933,935,938,941,944],{"class":282,"line":388},[280,917,918],{"class":436},"const",[280,920,921],{"class":317}," ThemeContext",[280,923,850],{"class":436},[280,925,926],{"class":293}," createContext",[280,928,929],{"class":297},"\u003C",[280,931,932],{"class":293},"ThemeContextValue",[280,934,856],{"class":436},[280,936,937],{"class":317}," null",[280,939,940],{"class":297},">(",[280,942,943],{"class":317},"null",[280,945,656],{"class":297},[280,947,948],{"class":282,"line":401},[280,949,424],{"emptyLinePlaceholder":423},[280,951,952,955,958,961,964,967,970,972,975,977,979,982,984,987],{"class":282,"line":414},[280,953,954],{"class":436},"export",[280,956,957],{"class":436}," function",[280,959,960],{"class":293}," ThemeProvider",[280,962,963],{"class":297},"({ ",[280,965,966],{"class":310},"children",[280,968,969],{"class":297}," }",[280,971,883],{"class":436},[280,973,974],{"class":297}," { ",[280,976,966],{"class":310},[280,978,883],{"class":436},[280,980,981],{"class":293}," React",[280,983,784],{"class":297},[280,985,986],{"class":293},"ReactNode",[280,988,989],{"class":297}," }) {\n",[280,991,992],{"class":282,"line":420},[280,993,994],{"class":286}," \u002F\u002F Initialize with system preference to prevent hydration mismatch\n",[280,996,997,1000,1003,1006,1008,1011,1014,1016,1019,1021,1024,1027,1029],{"class":282,"line":427},[280,998,999],{"class":436}," const",[280,1001,1002],{"class":297}," [",[280,1004,1005],{"class":317},"theme",[280,1007,248],{"class":297},[280,1009,1010],{"class":317},"setTheme",[280,1012,1013],{"class":297},"] ",[280,1015,437],{"class":436},[280,1017,1018],{"class":293}," useState",[280,1020,929],{"class":297},[280,1022,1023],{"class":293},"Theme",[280,1025,1026],{"class":297},">(() ",[280,1028,900],{"class":436},[280,1030,298],{"class":297},[280,1032,1033,1036,1039,1042,1045,1048,1051],{"class":282,"line":447},[280,1034,1035],{"class":436}," if",[280,1037,1038],{"class":297}," (",[280,1040,1041],{"class":436},"typeof",[280,1043,1044],{"class":297}," window ",[280,1046,1047],{"class":436},"!==",[280,1049,1050],{"class":440}," 'undefined'",[280,1052,1053],{"class":297},") {\n",[280,1055,1056,1059,1062,1065,1067,1070,1073,1076,1078,1081,1083],{"class":282,"line":459},[280,1057,1058],{"class":436}," return",[280,1060,1061],{"class":297}," window.",[280,1063,1064],{"class":293},"matchMedia",[280,1066,651],{"class":297},[280,1068,1069],{"class":440},"'(prefers-color-scheme: dark)'",[280,1071,1072],{"class":297},").matches ",[280,1074,1075],{"class":436},"?",[280,1077,859],{"class":440},[280,1079,1080],{"class":436}," :",[280,1082,853],{"class":440},[280,1084,321],{"class":297},[280,1086,1087],{"class":282,"line":471},[280,1088,613],{"class":297},[280,1090,1091,1093,1095],{"class":282,"line":483},[280,1092,1058],{"class":436},[280,1094,853],{"class":440},[280,1096,321],{"class":297},[280,1098,1099],{"class":282,"line":495},[280,1100,1101],{"class":297}," });\n",[280,1103,1104],{"class":282,"line":507},[280,1105,424],{"emptyLinePlaceholder":423},[280,1107,1108,1111,1114,1116],{"class":282,"line":519},[280,1109,1110],{"class":293}," useEffect",[280,1112,1113],{"class":297},"(() ",[280,1115,900],{"class":436},[280,1117,298],{"class":297},[280,1119,1120,1122,1125,1127],{"class":282,"line":531},[280,1121,999],{"class":436},[280,1123,1124],{"class":317}," root",[280,1126,850],{"class":436},[280,1128,1129],{"class":297}," document.documentElement;\n",[280,1131,1132,1135,1138,1140,1143],{"class":282,"line":536},[280,1133,1134],{"class":297}," root.",[280,1136,1137],{"class":293},"setAttribute",[280,1139,651],{"class":297},[280,1141,1142],{"class":440},"'data-theme'",[280,1144,1145],{"class":297},", theme);\n",[280,1147,1148],{"class":282,"line":541},[280,1149,379],{"class":297},[280,1151,1152],{"class":282,"line":547},[280,1153,1154],{"class":286}," \u002F\u002F Announce theme change to screen readers\n",[280,1156,1157,1159,1162,1164,1167,1170,1172,1175],{"class":282,"line":556},[280,1158,999],{"class":436},[280,1160,1161],{"class":317}," statusEl",[280,1163,850],{"class":436},[280,1165,1166],{"class":297}," document.",[280,1168,1169],{"class":293},"getElementById",[280,1171,651],{"class":297},[280,1173,1174],{"class":440},"'a11y-theme-status'",[280,1176,656],{"class":297},[280,1178,1179,1181],{"class":282,"line":573},[280,1180,1035],{"class":436},[280,1182,1183],{"class":297}," (statusEl) {\n",[280,1185,1186,1189,1191,1194,1196,1199],{"class":282,"line":581},[280,1187,1188],{"class":297}," statusEl.textContent ",[280,1190,437],{"class":436},[280,1192,1193],{"class":440}," `Theme switched to ${",[280,1195,1005],{"class":297},[280,1197,1198],{"class":440},"} mode`",[280,1200,321],{"class":297},[280,1202,1203],{"class":282,"line":589},[280,1204,613],{"class":297},[280,1206,1207],{"class":282,"line":597},[280,1208,1209],{"class":297}," }, [theme]);\n",[280,1211,1212],{"class":282,"line":610},[280,1213,424],{"emptyLinePlaceholder":423},[280,1215,1216,1218,1220,1222,1224,1226,1229,1231,1234,1237,1240,1243,1245,1248,1250,1252,1254],{"class":282,"line":616},[280,1217,999],{"class":436},[280,1219,892],{"class":293},[280,1221,850],{"class":436},[280,1223,897],{"class":297},[280,1225,900],{"class":436},[280,1227,1228],{"class":293}," setTheme",[280,1230,651],{"class":297},[280,1232,1233],{"class":310},"prev",[280,1235,1236],{"class":436}," =>",[280,1238,1239],{"class":297}," prev ",[280,1241,1242],{"class":436},"===",[280,1244,853],{"class":440},[280,1246,1247],{"class":436}," ?",[280,1249,859],{"class":440},[280,1251,1080],{"class":436},[280,1253,853],{"class":440},[280,1255,656],{"class":297},[280,1257,1258],{"class":282,"line":621},[280,1259,424],{"emptyLinePlaceholder":423},[280,1261,1262,1264],{"class":282,"line":626},[280,1263,1058],{"class":436},[280,1265,1266],{"class":297}," (\n",[280,1268,1269,1272,1275,1278,1280],{"class":282,"line":632},[280,1270,1271],{"class":297}," \u003C",[280,1273,1274],{"class":317},"ThemeContext.Provider",[280,1276,1277],{"class":293}," value",[280,1279,437],{"class":436},[280,1281,1282],{"class":297},"{{ theme, toggleTheme }}>\n",[280,1284,1285,1288,1291],{"class":282,"line":640},[280,1286,1287],{"class":297}," {",[280,1289,1290],{"class":286},"\u002F* Live region for AT announcements *\u002F",[280,1292,417],{"class":297},[280,1294,1295,1297,1301,1304,1306,1309,1312,1314,1317,1320,1322,1325,1328,1330,1333],{"class":282,"line":659},[280,1296,1271],{"class":297},[280,1298,1300],{"class":1299},"s9eBZ","div",[280,1302,1303],{"class":293}," id",[280,1305,437],{"class":436},[280,1307,1308],{"class":440},"\"a11y-theme-status\"",[280,1310,1311],{"class":293}," role",[280,1313,437],{"class":436},[280,1315,1316],{"class":440},"\"status\"",[280,1318,1319],{"class":293}," aria-live",[280,1321,437],{"class":436},[280,1323,1324],{"class":440},"\"polite\"",[280,1326,1327],{"class":293}," className",[280,1329,437],{"class":436},[280,1331,1332],{"class":440},"\"sr-only\"",[280,1334,1335],{"class":297}," \u002F>\n",[280,1337,1338],{"class":282,"line":675},[280,1339,1340],{"class":297}," {children}\n",[280,1342,1343,1346,1348],{"class":282,"line":702},[280,1344,1345],{"class":297}," \u003C\u002F",[280,1347,1274],{"class":317},[280,1349,1350],{"class":297},">\n",[280,1352,1353],{"class":282,"line":734},[280,1354,1355],{"class":297}," );\n",[280,1357,1359],{"class":282,"line":1358},42,[280,1360,417],{"class":297},[280,1362,1364],{"class":282,"line":1363},43,[280,1365,424],{"emptyLinePlaceholder":423},[280,1367,1369,1371,1373,1376,1378,1380,1382],{"class":282,"line":1368},44,[280,1370,954],{"class":436},[280,1372,999],{"class":436},[280,1374,1375],{"class":293}," useTheme",[280,1377,850],{"class":436},[280,1379,897],{"class":297},[280,1381,900],{"class":436},[280,1383,298],{"class":297},[280,1385,1387,1389,1392,1394,1397],{"class":282,"line":1386},45,[280,1388,999],{"class":436},[280,1390,1391],{"class":317}," context",[280,1393,850],{"class":436},[280,1395,1396],{"class":293}," useContext",[280,1398,1399],{"class":297},"(ThemeContext);\n",[280,1401,1403,1405,1407,1410,1413,1416,1419,1422,1424,1427],{"class":282,"line":1402},46,[280,1404,1035],{"class":436},[280,1406,1038],{"class":297},[280,1408,1409],{"class":436},"!",[280,1411,1412],{"class":297},"context) ",[280,1414,1415],{"class":436},"throw",[280,1417,1418],{"class":436}," new",[280,1420,1421],{"class":293}," Error",[280,1423,651],{"class":297},[280,1425,1426],{"class":440},"'useTheme must be used within ThemeProvider'",[280,1428,656],{"class":297},[280,1430,1432,1434],{"class":282,"line":1431},47,[280,1433,1058],{"class":436},[280,1435,1436],{"class":297}," context;\n",[280,1438,1440],{"class":282,"line":1439},48,[280,1441,1442],{"class":297},"};\n",[166,1444,1445,1446,1449,1450,1453,1454,1457],{},"When implementing theme toggles, adhere to ",[170,1447,61],{"href":1448},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002F"," by using native ",[245,1451,1452],{},"\u003Cbutton>"," elements with ",[245,1455,1456],{},"aria-pressed"," rather than custom divs with click handlers.",[166,1459,1460,1462,1463,1465,1466,1469],{},[186,1461,741],{}," Verify that theme updates do not disrupt DOM order or trigger unexpected focus loss. Assert ",[245,1464,1456],{}," state toggles correctly and that ",[245,1467,1468],{},"role=\"status\""," receives the expected announcement in screen reader automation (e.g., Playwright + NVDA\u002FJAWS).",[224,1471],{},[227,1473,1475],{"id":1474},"runtime-contrast-validation-fallbacks","Runtime Contrast Validation & Fallbacks",[166,1477,1478],{},"Static tokens cannot always predict dynamic content scenarios. User-generated content, third-party widgets, or framework portals may inject low-contrast elements at runtime. Implementing client-side safeguards ensures compliance before accessibility regressions impact users.",[166,1480,1481],{},[186,1482,1483],{},"Implementation Strategy:",[180,1485,1486,1496,1502],{},[183,1487,258,1488,1491,1492,1495],{},[245,1489,1490],{},"ResizeObserver"," or ",[245,1493,1494],{},"MutationObserver"," to trigger contrast checks when dynamic content mounts.",[183,1497,1498,1499,784],{},"Provide user-controlled contrast overrides persisted via ",[245,1500,1501],{},"localStorage",[183,1503,1504],{},"Gracefully degrade to high-contrast fallbacks when custom themes violate WCAG thresholds.",[271,1506,1510],{"className":1507,"code":1508,"language":1509,"meta":276,"style":276},"language-vue shiki shiki-themes github-light github-dark","\u003C!-- useAccessibleTheme.ts (Vue 3 Composable) -->\nimport { ref, watch, onMounted, nextTick } from 'vue';\n\n\u002F\u002F WCAG 2.2 Relative Luminance Calculation\nfunction getLuminance(hex: string): number {\n const rgb = hex.replace('#', '').match(\u002F.{2}\u002Fg)?.map(c => {\n const val = parseInt(c, 16) \u002F 255;\n return val \u003C= 0.03928 ? val \u002F 12.92 : Math.pow((val + 0.055) \u002F 1.055, 2.4);\n }) || [0, 0, 0];\n return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];\n}\n\nfunction getContrastRatio(fg: string, bg: string): number {\n const lum1 = getLuminance(fg);\n const lum2 = getLuminance(bg);\n const lighter = Math.max(lum1, lum2);\n const darker = Math.min(lum1, lum2);\n return (lighter + 0.05) \u002F (darker + 0.05);\n}\n\nexport function useAccessibleTheme() {\n const currentTheme = ref\u003C'light' | 'dark'>('light');\n const contrastRatio = ref\u003Cnumber>(4.5);\n const isFallbackActive = ref(false);\n\n const validateComputedStyles = () => {\n const root = document.documentElement;\n const bg = getComputedStyle(root).getPropertyValue('--color-surface').trim();\n const fg = getComputedStyle(root).getPropertyValue('--color-text-primary').trim();\n \n if (!bg || !fg) return;\n\n const ratio = getContrastRatio(fg, bg);\n contrastRatio.value = ratio;\n \n \u002F\u002F Enforce fallback if below WCAG AA minimum\n if (ratio \u003C 4.5) {\n isFallbackActive.value = true;\n root.style.setProperty('--color-text-primary', 'var(--color-text-primary-fallback, #000000)');\n } else {\n isFallbackActive.value = false;\n }\n };\n\n onMounted(() => {\n validateComputedStyles();\n \u002F\u002F Watch for dynamic CSS variable changes (e.g., from third-party scripts)\n const observer = new MutationObserver(validateComputedStyles);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });\n });\n\n return { currentTheme, contrastRatio, isFallbackActive };\n}\n","vue",[245,1511,1512,1517,1522,1526,1531,1536,1541,1546,1590,1607,1640,1644,1648,1670,1683,1695,1710,1724,1746,1750,1754,1765,1787,1792,1797,1801,1806,1811,1816,1821,1825,1830,1834,1839,1844,1848,1853,1858,1863,1868,1873,1878,1882,1887,1891,1896,1901,1906,1911,1917,1922,1927,1933],{"__ignoreMap":276},[280,1513,1514],{"class":282,"line":283},[280,1515,1516],{"class":286},"\u003C!-- useAccessibleTheme.ts (Vue 3 Composable) -->\n",[280,1518,1519],{"class":282,"line":290},[280,1520,1521],{"class":297},"import { ref, watch, onMounted, nextTick } from 'vue';\n",[280,1523,1524],{"class":282,"line":301},[280,1525,424],{"emptyLinePlaceholder":423},[280,1527,1528],{"class":282,"line":307},[280,1529,1530],{"class":297},"\u002F\u002F WCAG 2.2 Relative Luminance Calculation\n",[280,1532,1533],{"class":282,"line":324},[280,1534,1535],{"class":297},"function getLuminance(hex: string): number {\n",[280,1537,1538],{"class":282,"line":337},[280,1539,1540],{"class":297}," const rgb = hex.replace('#', '').match(\u002F.{2}\u002Fg)?.map(c => {\n",[280,1542,1543],{"class":282,"line":350},[280,1544,1545],{"class":297}," const val = parseInt(c, 16) \u002F 255;\n",[280,1547,1548,1551,1554,1556,1559,1561,1564,1567,1570,1573,1576,1579,1582,1584,1587],{"class":282,"line":363},[280,1549,1550],{"class":297}," return val \u003C= ",[280,1552,1553],{"class":1299},"0",[280,1555,784],{"class":297},[280,1557,1558],{"class":293},"03928",[280,1560,1247],{"class":293},[280,1562,1563],{"class":293}," val",[280,1565,1566],{"class":604}," \u002F",[280,1568,1569],{"class":293}," 12.92",[280,1571,1572],{"class":297}," : ",[280,1574,1575],{"class":293},"Math.pow((val",[280,1577,1578],{"class":293}," +",[280,1580,1581],{"class":293}," 0.055)",[280,1583,1566],{"class":604},[280,1585,1586],{"class":293}," 1.055,",[280,1588,1589],{"class":293}," 2.4);\n",[280,1591,1592,1595,1598,1601,1604],{"class":282,"line":376},[280,1593,1594],{"class":293}," })",[280,1596,1597],{"class":293}," ||",[280,1599,1600],{"class":293}," [0,",[280,1602,1603],{"class":293}," 0,",[280,1605,1606],{"class":293}," 0];\n",[280,1608,1609,1611,1614,1617,1620,1622,1625,1627,1630,1632,1635,1637],{"class":282,"line":382},[280,1610,1058],{"class":293},[280,1612,1613],{"class":293}," 0.2126",[280,1615,1616],{"class":293}," *",[280,1618,1619],{"class":293}," rgb[0]",[280,1621,1578],{"class":293},[280,1623,1624],{"class":293}," 0.7152",[280,1626,1616],{"class":293},[280,1628,1629],{"class":293}," rgb[1]",[280,1631,1578],{"class":293},[280,1633,1634],{"class":293}," 0.0722",[280,1636,1616],{"class":293},[280,1638,1639],{"class":293}," rgb[2];\n",[280,1641,1642],{"class":282,"line":388},[280,1643,417],{"class":293},[280,1645,1646],{"class":282,"line":401},[280,1647,424],{"emptyLinePlaceholder":423},[280,1649,1650,1653,1656,1659,1662,1665,1668],{"class":282,"line":414},[280,1651,1652],{"class":293},"function",[280,1654,1655],{"class":293}," getContrastRatio(fg:",[280,1657,1658],{"class":293}," string,",[280,1660,1661],{"class":293}," bg:",[280,1663,1664],{"class":293}," string):",[280,1666,1667],{"class":293}," number",[280,1669,298],{"class":293},[280,1671,1672,1674,1677,1680],{"class":282,"line":420},[280,1673,999],{"class":293},[280,1675,1676],{"class":293}," lum1",[280,1678,1679],{"class":297}," = ",[280,1681,1682],{"class":440},"getLuminance(fg);\n",[280,1684,1685,1687,1690,1692],{"class":282,"line":427},[280,1686,999],{"class":293},[280,1688,1689],{"class":293}," lum2",[280,1691,1679],{"class":297},[280,1693,1694],{"class":440},"getLuminance(bg);\n",[280,1696,1697,1699,1702,1704,1707],{"class":282,"line":447},[280,1698,999],{"class":293},[280,1700,1701],{"class":293}," lighter",[280,1703,1679],{"class":297},[280,1705,1706],{"class":440},"Math.max(lum1,",[280,1708,1709],{"class":293}," lum2);\n",[280,1711,1712,1714,1717,1719,1722],{"class":282,"line":459},[280,1713,999],{"class":293},[280,1715,1716],{"class":293}," darker",[280,1718,1679],{"class":297},[280,1720,1721],{"class":440},"Math.min(lum1,",[280,1723,1709],{"class":293},[280,1725,1726,1728,1731,1733,1736,1738,1741,1743],{"class":282,"line":471},[280,1727,1058],{"class":293},[280,1729,1730],{"class":293}," (lighter",[280,1732,1578],{"class":293},[280,1734,1735],{"class":293}," 0.05)",[280,1737,1566],{"class":604},[280,1739,1740],{"class":293}," (darker",[280,1742,1578],{"class":293},[280,1744,1745],{"class":293}," 0.05);\n",[280,1747,1748],{"class":282,"line":483},[280,1749,417],{"class":293},[280,1751,1752],{"class":282,"line":495},[280,1753,424],{"emptyLinePlaceholder":423},[280,1755,1756,1758,1760,1763],{"class":282,"line":507},[280,1757,954],{"class":293},[280,1759,957],{"class":293},[280,1761,1762],{"class":293}," useAccessibleTheme()",[280,1764,298],{"class":293},[280,1766,1767,1769,1772,1774,1777,1780,1782,1784],{"class":282,"line":519},[280,1768,999],{"class":293},[280,1770,1771],{"class":293}," currentTheme",[280,1773,1679],{"class":297},[280,1775,1776],{"class":440},"ref",[280,1778,1779],{"class":604},"\u003C'light'",[280,1781,856],{"class":293},[280,1783,859],{"class":604},[280,1785,1786],{"class":297},">('light');\n",[280,1788,1789],{"class":282,"line":531},[280,1790,1791],{"class":297}," const contrastRatio = ref\u003Cnumber>(4.5);\n",[280,1793,1794],{"class":282,"line":536},[280,1795,1796],{"class":297}," const isFallbackActive = ref(false);\n",[280,1798,1799],{"class":282,"line":541},[280,1800,424],{"emptyLinePlaceholder":423},[280,1802,1803],{"class":282,"line":547},[280,1804,1805],{"class":297}," const validateComputedStyles = () => {\n",[280,1807,1808],{"class":282,"line":556},[280,1809,1810],{"class":297}," const root = document.documentElement;\n",[280,1812,1813],{"class":282,"line":573},[280,1814,1815],{"class":297}," const bg = getComputedStyle(root).getPropertyValue('--color-surface').trim();\n",[280,1817,1818],{"class":282,"line":581},[280,1819,1820],{"class":297}," const fg = getComputedStyle(root).getPropertyValue('--color-text-primary').trim();\n",[280,1822,1823],{"class":282,"line":589},[280,1824,379],{"class":297},[280,1826,1827],{"class":282,"line":597},[280,1828,1829],{"class":297}," if (!bg || !fg) return;\n",[280,1831,1832],{"class":282,"line":610},[280,1833,424],{"emptyLinePlaceholder":423},[280,1835,1836],{"class":282,"line":616},[280,1837,1838],{"class":297}," const ratio = getContrastRatio(fg, bg);\n",[280,1840,1841],{"class":282,"line":621},[280,1842,1843],{"class":297}," contrastRatio.value = ratio;\n",[280,1845,1846],{"class":282,"line":626},[280,1847,379],{"class":297},[280,1849,1850],{"class":282,"line":632},[280,1851,1852],{"class":297}," \u002F\u002F Enforce fallback if below WCAG AA minimum\n",[280,1854,1855],{"class":282,"line":640},[280,1856,1857],{"class":297}," if (ratio \u003C 4.5) {\n",[280,1859,1860],{"class":282,"line":659},[280,1861,1862],{"class":297}," isFallbackActive.value = true;\n",[280,1864,1865],{"class":282,"line":675},[280,1866,1867],{"class":297}," root.style.setProperty('--color-text-primary', 'var(--color-text-primary-fallback, #000000)');\n",[280,1869,1870],{"class":282,"line":702},[280,1871,1872],{"class":297}," } else {\n",[280,1874,1875],{"class":282,"line":734},[280,1876,1877],{"class":297}," isFallbackActive.value = false;\n",[280,1879,1880],{"class":282,"line":1358},[280,1881,613],{"class":297},[280,1883,1884],{"class":282,"line":1363},[280,1885,1886],{"class":297}," };\n",[280,1888,1889],{"class":282,"line":1368},[280,1890,424],{"emptyLinePlaceholder":423},[280,1892,1893],{"class":282,"line":1386},[280,1894,1895],{"class":297}," onMounted(() => {\n",[280,1897,1898],{"class":282,"line":1402},[280,1899,1900],{"class":297}," validateComputedStyles();\n",[280,1902,1903],{"class":282,"line":1431},[280,1904,1905],{"class":297}," \u002F\u002F Watch for dynamic CSS variable changes (e.g., from third-party scripts)\n",[280,1907,1908],{"class":282,"line":1439},[280,1909,1910],{"class":297}," const observer = new MutationObserver(validateComputedStyles);\n",[280,1912,1914],{"class":282,"line":1913},49,[280,1915,1916],{"class":297}," observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });\n",[280,1918,1920],{"class":282,"line":1919},50,[280,1921,1101],{"class":297},[280,1923,1925],{"class":282,"line":1924},51,[280,1926,424],{"emptyLinePlaceholder":423},[280,1928,1930],{"class":282,"line":1929},52,[280,1931,1932],{"class":297}," return { currentTheme, contrastRatio, isFallbackActive };\n",[280,1934,1936],{"class":282,"line":1935},53,[280,1937,417],{"class":297},[166,1939,1940,1942,1943,1946],{},[186,1941,741],{}," Test edge cases where user-generated content or third-party widgets inject low-contrast elements. Use Cypress\u002FPlaywright to inject inline styles and assert that ",[245,1944,1945],{},"isFallbackActive"," triggers and computed contrast ratios meet thresholds.",[224,1948],{},[227,1950,1952],{"id":1951},"high-contrast-mode-system-preferences-integration","High Contrast Mode & System Preferences Integration",[166,1954,1955],{},"Modern operating systems provide forced-colors and enhanced-contrast modes that override author styles. Respecting these settings is non-negotiable for compliance and requires explicit CSS media query handling.",[166,1957,1958],{},[186,1959,238],{},[180,1961,1962,1972,1975],{},[183,1963,1964,1965,262,1968,1971],{},"Implement ",[245,1966,1967],{},"prefers-contrast",[245,1969,1970],{},"forced-colors"," media queries to adapt token values.",[183,1973,1974],{},"Avoid relying on color alone to convey state, validation, or interactivity.",[183,1976,1977,1978,1981],{},"Coordinate theme transitions with ",[170,1979,25],{"href":1980},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002F"," to maintain keyboard navigation continuity during mode switches.",[271,1983,1985],{"className":273,"code":1984,"language":275,"meta":276,"style":276},"\u002F* forced-colors.css *\u002F\n@media (forced-colors: active) {\n \u002F* System colors override custom tokens *\u002F\n :root {\n --color-surface: Canvas;\n --color-text-primary: CanvasText;\n --color-border: ButtonBorder;\n --color-focus-ring: Highlight;\n }\n\n \u002F* Ensure non-text contrast meets 3:1 requirement *\u002F\n .icon, .svg-icon {\n fill: CanvasText;\n stroke: CanvasText;\n }\n\n \u002F* Preserve focus visibility *\u002F\n :focus-visible {\n outline: 2px solid Highlight;\n outline-offset: 2px;\n }\n\n \u002F* Disable custom transitions that interfere with system rendering *\u002F\n * {\n transition: none !important;\n }\n}\n",[245,1986,1987,1992,1999,2004,2010,2016,2022,2028,2038,2042,2046,2051,2063,2070,2077,2081,2085,2090,2097,2116,2129,2133,2137,2142,2148,2162,2166],{"__ignoreMap":276},[280,1988,1989],{"class":282,"line":283},[280,1990,1991],{"class":286},"\u002F* forced-colors.css *\u002F\n",[280,1993,1994,1996],{"class":282,"line":290},[280,1995,550],{"class":436},[280,1997,1998],{"class":297}," (forced-colors: active) {\n",[280,2000,2001],{"class":282,"line":301},[280,2002,2003],{"class":286}," \u002F* System colors override custom tokens *\u002F\n",[280,2005,2006,2008],{"class":282,"line":307},[280,2007,559],{"class":293},[280,2009,298],{"class":297},[280,2011,2012,2014],{"class":282,"line":324},[280,2013,311],{"class":310},[280,2015,586],{"class":297},[280,2017,2018,2020],{"class":282,"line":337},[280,2019,327],{"class":310},[280,2021,578],{"class":297},[280,2023,2024,2026],{"class":282,"line":350},[280,2025,353],{"class":310},[280,2027,594],{"class":297},[280,2029,2030,2032,2034,2036],{"class":282,"line":363},[280,2031,366],{"class":310},[280,2033,314],{"class":297},[280,2035,605],{"class":604},[280,2037,321],{"class":297},[280,2039,2040],{"class":282,"line":376},[280,2041,613],{"class":297},[280,2043,2044],{"class":282,"line":382},[280,2045,424],{"emptyLinePlaceholder":423},[280,2047,2048],{"class":282,"line":388},[280,2049,2050],{"class":286}," \u002F* Ensure non-text contrast meets 3:1 requirement *\u002F\n",[280,2052,2053,2056,2058,2061],{"class":282,"line":401},[280,2054,2055],{"class":293}," .icon",[280,2057,248],{"class":297},[280,2059,2060],{"class":293},".svg-icon",[280,2062,298],{"class":297},[280,2064,2065,2068],{"class":282,"line":414},[280,2066,2067],{"class":317}," fill",[280,2069,578],{"class":297},[280,2071,2072,2075],{"class":282,"line":420},[280,2073,2074],{"class":317}," stroke",[280,2076,578],{"class":297},[280,2078,2079],{"class":282,"line":427},[280,2080,613],{"class":297},[280,2082,2083],{"class":282,"line":447},[280,2084,424],{"emptyLinePlaceholder":423},[280,2086,2087],{"class":282,"line":459},[280,2088,2089],{"class":286}," \u002F* Preserve focus visibility *\u002F\n",[280,2091,2092,2095],{"class":282,"line":471},[280,2093,2094],{"class":293}," :focus-visible",[280,2096,298],{"class":297},[280,2098,2099,2102,2104,2107,2109,2111,2114],{"class":282,"line":483},[280,2100,2101],{"class":317}," outline",[280,2103,314],{"class":297},[280,2105,2106],{"class":317},"2",[280,2108,686],{"class":436},[280,2110,689],{"class":317},[280,2112,2113],{"class":604}," Highlight",[280,2115,321],{"class":297},[280,2117,2118,2121,2123,2125,2127],{"class":282,"line":495},[280,2119,2120],{"class":317}," outline-offset",[280,2122,314],{"class":297},[280,2124,2106],{"class":317},[280,2126,686],{"class":436},[280,2128,321],{"class":297},[280,2130,2131],{"class":282,"line":507},[280,2132,613],{"class":297},[280,2134,2135],{"class":282,"line":519},[280,2136,424],{"emptyLinePlaceholder":423},[280,2138,2139],{"class":282,"line":531},[280,2140,2141],{"class":286}," \u002F* Disable custom transitions that interfere with system rendering *\u002F\n",[280,2143,2144,2146],{"class":282,"line":536},[280,2145,1616],{"class":1299},[280,2147,298],{"class":297},[280,2149,2150,2152,2154,2157,2160],{"class":282,"line":541},[280,2151,705],{"class":317},[280,2153,314],{"class":297},[280,2155,2156],{"class":317},"none",[280,2158,2159],{"class":436}," !important",[280,2161,321],{"class":297},[280,2163,2164],{"class":282,"line":547},[280,2165,613],{"class":297},[280,2167,2168],{"class":282,"line":556},[280,2169,417],{"class":297},[166,2171,2172,2175,2176,2179,2180,2183,2184,2187,2188,2190],{},[186,2173,2174],{},"Framework Note:"," React portals and Vue ",[245,2177,2178],{},"\u003CTeleport>"," targets may lose forced-colors inheritance if they render outside the ",[245,2181,2182],{},"\u003Chtml>"," context. Always mount portals as direct children of ",[245,2185,2186],{},"document.body"," and verify computed styles in ",[245,2189,1970],{}," simulation.",[166,2192,2193,2195,2196,2199,2200,2203],{},[186,2194,741],{}," Use browser devtools to simulate ",[245,2197,2198],{},"forced-colors: active"," mode. Verify border\u002Ficon visibility, focus ring contrast, and that ",[245,2201,2202],{},"!important"," overrides do not suppress system accessibility settings.",[224,2205],{},[227,2207,2209],{"id":2208},"common-implementation-pitfalls","Common Implementation Pitfalls",[2211,2212,2213,2219,2225,2235,2246],"ol",{},[183,2214,2215,2218],{},[186,2216,2217],{},"Hardcoding hex values in component styles:"," Breaks the token cascade and prevents runtime contrast validation. Always reference semantic CSS variables.",[183,2220,2221,2224],{},[186,2222,2223],{},"Ignoring non-text contrast requirements:"," Form borders, icons, and focus indicators require 3:1 contrast. Relying solely on text contrast leaves interactive elements inaccessible.",[183,2226,2227,2230,2231,2234],{},[186,2228,2229],{},"Using opacity for contrast variations:"," ",[245,2232,2233],{},"opacity"," alters the alpha channel, which fails WCAG relative luminance calculations. Use explicit color tokens instead.",[183,2236,2237,2240,2241,2243,2244,784],{},[186,2238,2239],{},"Failing to persist user preferences:"," Theme state must survive route changes and sessions. Use ",[245,2242,1501],{}," with fallback to ",[245,2245,783],{},[183,2247,2248,2256,2257,2259],{},[186,2249,2250,2251,2253,2254,883],{},"Overriding ",[245,2252,1970],{}," with ",[245,2255,2202],{}," Suppresses OS-level accessibility settings. Only use ",[245,2258,2202],{}," for explicit fallbacks, never to block system contrast modes.",[224,2261],{},[227,2263,2265],{"id":2264},"frequently-asked-questions","Frequently Asked Questions",[166,2267,2268,2271],{},[186,2269,2270],{},"How do I enforce WCAG contrast ratios in a dynamic theming system?","\nUse CSS custom properties mapped to semantic tokens, then implement a build-time or runtime contrast checker that validates foreground\u002Fbackground pairs against WCAG 2.2 thresholds. Provide fallback tokens that automatically activate when user-generated themes fall below 4.5:1 for text or 3:1 for UI components.",[166,2273,2274,2280,2281,2283,2284,1491,2286,2288],{},[186,2275,2276,2277,2279],{},"Does ",[245,2278,783],{}," automatically handle accessibility requirements?","\nNo. ",[245,2282,783],{}," only detects the user's OS preference. It does not guarantee WCAG-compliant contrast ratios. You must explicitly test both light and dark palettes, and implement ",[245,2285,1967],{},[245,2287,1970],{}," media queries to support users who require enhanced contrast beyond standard dark mode.",[166,2290,2291,2294],{},[186,2292,2293],{},"Can I use JavaScript to calculate contrast ratios dynamically?","\nYes, but it should be a secondary validation layer. Primary contrast enforcement should happen at the CSS\u002Fdesign-token level to avoid layout shifts and performance overhead. Use JavaScript only for runtime overrides, user preference persistence, or dynamic content injection where static tokens cannot predict color combinations.",[166,2296,2297,2300,2301,2304],{},[186,2298,2299],{},"How do theme transitions impact keyboard navigation and screen readers?","\nPoorly implemented transitions can cause focus loss, DOM reordering, or sudden style changes that confuse assistive technologies. Always preserve focus state during theme switches, use CSS transitions instead of JavaScript-driven DOM manipulation, and ensure theme toggles announce state changes via ",[245,2302,2303],{},"aria-live"," or native button semantics.",[771,2306,2307],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":276,"searchDepth":290,"depth":290,"links":2309},[2310,2311,2312,2313,2314,2315,2316,2317],{"id":177,"depth":301,"text":178},{"id":210,"depth":301,"text":211},{"id":229,"depth":290,"text":230},{"id":751,"depth":290,"text":752},{"id":1474,"depth":290,"text":1475},{"id":1951,"depth":290,"text":1952},{"id":2208,"depth":290,"text":2209},{"id":2264,"depth":290,"text":2265},null,"Design accessible color systems with contrast-safe tokens, adaptive theming, and preference-aware CSS strategies for inclusive interfaces.","md",{},false,{"title":13,"description":2319},"ixzPlWQl8g_xaRXawEvPrhS7LV8quopdr5LjhB4P-cI",[2326,2356,2357],{"title":5,"path":6,"stem":7,"children":2327},[2328,2329,2332,2335,2341,2347,2353],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2330},[2331],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2333},[2334],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2336},[2337,2338],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2339},[2340],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2342},[2343,2344],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2345},[2346],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2348},[2349,2350],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2351},[2352],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2354},[2355],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},{"title":71,"path":72,"stem":73,"children":2358},[2359,2360,2366,2372,2375,2384,2393],{"title":76,"path":72,"stem":77},{"title":79,"path":80,"stem":81,"children":2361},[2362,2363],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":2364},[2365],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":2367},[2368,2369],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":2370},[2371],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2373},[2374],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2376},[2377,2378,2381],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2379},[2380],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2382},[2383],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2385},[2386,2387,2390],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2388},[2389],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2391},[2392],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2394},[2395,2396],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2397},[2398],{"title":151,"path":152,"stem":153},1778094796117]