[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-forced-colors-high-contrast-mode":93,"path-membership-forced-colors-high-contrast-mode":276,"related-forced-colors-high-contrast-mode":277,"code:css:true:vpheze":407,"code:css:true:o17ku6":408,"comments-01a133e2-3e89-757c-ae01-38a8ad8aa448":409},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,[45,49,53,57,61,65,69,73,77,81,85,89],{"slug":46,"name":47,"articles":48},"webdev","Webdev",144,{"slug":50,"name":51,"articles":52},"javascript","Javascript",117,{"slug":54,"name":55,"articles":56},"frontend","Frontend",87,{"slug":58,"name":59,"articles":60},"tutorial","Tutorial",55,{"slug":62,"name":63,"articles":64},"css","Css",44,{"slug":66,"name":67,"articles":68},"performance","Performance",23,{"slug":70,"name":71,"articles":72},"typescript","Typescript",19,{"slug":74,"name":75,"articles":76},"react","React",18,{"slug":78,"name":79,"articles":80},"browser","Browser",13,{"slug":82,"name":83,"articles":84},"node","Node",12,{"slug":86,"name":87,"articles":88},"programming","Programming",11,{"slug":90,"name":91,"articles":92},"accessibility","Accessibility",10,{"id":94,"slug":95,"title":96,"subtitle":97,"excerpt":98,"coverUrl":99,"locale":13,"readingMinutes":100,"publishedAt":101,"viewCount":102,"likeCount":19,"commentCount":19,"author":103,"vertical":108,"topic":109,"tags":111,"_count":116,"playground":118,"body":120,"bodyMd":260,"seo":261,"translationGroupId":264,"series":97,"podcastUrl":97,"verticalId":5,"thread":265,"assessments":267,"translations":272,"quiz":274},"01a133e2-3e89-757c-ae01-38a8ad8aa448","forced-colors-high-contrast-mode","Stop Shipping Custom UI That Disappears in `forced-colors` Mode",null,"Windows High Contrast Mode doesn't just boost contrast — it strips your backgrounds, shadows, and colors outright. Here's what `forced-colors` actually does, why a styled \u003Cdiv> vanishes while a real \u003Cbutton> survives, and the fix.","\u002Fmedia\u002Fcovers\u002Fforced-colors-high-contrast-mode.png",7,"2026-10-18T10:15:03.062Z",27,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},"CSS",[112,113,114,115],{"slug":62,"name":63,"color":97},{"slug":90,"name":91,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"assessments":117},1,{"slug":95,"title":119},"forced-colors mode — interactive playground",{"blocks":121,"version":117},[122,126,129,132,137,140,143,146,150,153,158,161,164,167,170,174,177,180,183,186,190,193,196,199,202,205,208,211,214,217,220,223,226,229,232,235,238,241,244,247,250,253],{"id":123,"html":124,"type":125},"b1","\u003Cp>A QA ticket landed with a screenshot: &quot;Submit button missing on checkout.&quot; I opened the page in Chrome. The button was right there — blue, rounded, impossible to miss. I opened it in Firefox. Same button. I assumed a bad screenshot and closed the ticket.\u003C\u002Fp>","paragraph",{"id":127,"html":128,"type":125},"b2","\u003Cp>Three days later it came back, same screenshot, same page, from a different tester. This time I asked what they&#39;d changed. &quot;Nothing,&quot; they said. &quot;I just use Windows with High Contrast on, because I&#39;m light-sensitive.&quot; I flipped that setting on my own machine for the first time in years, reloaded the page, and watched my button disappear into the background like it had never existed.\u003C\u002Fp>",{"id":130,"html":131,"type":125},"b3","\u003Cp>It wasn&#39;t a rendering bug. It wasn&#39;t a z-index fight. It was a CSS feature I&#39;d never once had to think about, quietly rewriting my entire color palette out from under me.\u003C\u002Fp>",{"id":133,"html":134,"text":135,"type":136,"level":31},"b4","The fix that didn&#39;t fix anything","The fix that didn't fix anything","heading",{"id":138,"html":139,"type":125},"b5","\u003Cp>My first guess was contrast. The button used \u003Ccode>background: #2563eb\u003C\u002Fcode> on a white card — readable, passed every automated contrast checker I ran. So I assumed High Contrast Mode was some kind of amplifier: it probably just needed \u003Cem>more\u003C\u002Fem> contrast to work with. I darkened the blue, added a heavier \u003Ccode>box-shadow\u003C\u002Fcode> for depth, bumped the border a shade, and shipped it.\u003C\u002Fp>",{"id":141,"html":142,"type":125},"b6","\u003Cp>The next build, the button was still gone.\u003C\u002Fp>",{"id":144,"html":145,"type":125},"b7","\u003Cp>That&#39;s the part that should have told me something was off: I hadn&#39;t made the colors \u003Cem>fail\u003C\u002Fem> a contrast check, I&#39;d made them \u003Cem>more\u003C\u002Fem> correct by every metric I knew how to check — and the bug didn&#39;t budge. If contrast were the problem, a darker blue should have helped. It changed nothing, because the browser was never reading my blue in the first place.\u003C\u002Fp>",{"id":147,"html":148,"text":149,"type":136,"level":31},"b8","What \u003Ccode>forced-colors\u003C\u002Fcode> actually does","What forced-colors actually does",{"id":151,"html":152,"type":125},"b9","\u003Cp>Windows High Contrast Mode doesn&#39;t ask the browser to render your page with better contrast. It tells the browser to render your page using the operating system&#39;s own fixed palette — full stop, author CSS or not. Every modern browser exposes this as a media feature:\u003C\u002Fp>",{"id":154,"code":155,"type":156,"language":62,"highlight":157},"b10","@media (forced-colors: active) {\n  \u002F* the user has an OS-level forced-colors palette turned on *\u002F\n}","code",[],{"id":159,"html":160,"type":125},"b11","\u003Cp>It only ever matches when the \u003Cem>user\u003C\u002Fem> has flipped an OS-level switch (Windows High Contrast themes are the classic trigger) — you can&#39;t turn it on with JavaScript, and \u003Ccode>prefers-contrast\u003C\u002Fcode> is a different, much gentler feature that just reports a preference for more or less contrast without touching your styles at all. \u003Ccode>forced-colors\u003C\u002Fcode> is not a hint. When it&#39;s active, the browser overrides nearly every color-related property you wrote — \u003Ccode>color\u003C\u002Fcode>, \u003Ccode>background-color\u003C\u002Fcode>, \u003Ccode>border-color\u003C\u002Fcode>, \u003Ccode>outline-color\u003C\u002Fcode> — and remaps each one onto one of a couple dozen system colors, chosen by what kind of element it&#39;s on. Normal text becomes \u003Ccode>CanvasText\u003C\u002Fcode> on a \u003Ccode>Canvas\u003C\u002Fcode> background. A button&#39;s label becomes \u003Ccode>ButtonText\u003C\u002Fcode>. None of that is something your stylesheet gets a vote on.\u003C\u002Fp>",{"id":162,"html":163,"type":125},"b12","\u003Cp>Two more things get treated even more bluntly: \u003Ccode>box-shadow\u003C\u002Fcode> and \u003Ccode>text-shadow\u003C\u002Fcode> are forced to \u003Ccode>none\u003C\u002Fcode>, and any \u003Ccode>background-image\u003C\u002Fcode> that isn&#39;t a \u003Ccode>url()\u003C\u002Fcode> is forced to \u003Ccode>none\u003C\u002Fcode> too. There&#39;s no sensible way to remap a drop shadow or a gradient onto a small fixed palette, so the browser doesn&#39;t try — it just removes them.\u003C\u002Fp>",{"id":165,"html":166,"type":125},"b13","\u003Cp>Here&#39;s the part that explains my exact bug. My button&#39;s shape came from \u003Ccode>background: #2563eb\u003C\u002Fcode> plus a \u003Ccode>box-shadow\u003C\u002Fcode> for depth, and it had \u003Ccode>border: none\u003C\u002Fcode>. Under forced colors: the background got remapped to some flat system color, the shadow vanished outright, and because I&#39;d never declared an actual border, there was nothing left for the browser to recolor into a visible edge. The button didn&#39;t get \u003Cem>uglier\u003C\u002Fem> — it got structurally invisible, blending straight into the page.\u003C\u002Fp>",{"id":168,"html":169,"type":125},"b14","\u003Cp>Quick gut check before you keep reading: if a native \u003Ccode>&lt;button&gt;\u003C\u002Fcode> has the exact same styles — \u003Ccode>background\u003C\u002Fcode>, \u003Ccode>box-shadow\u003C\u002Fcode>, \u003Ccode>border: none\u003C\u002Fcode> — why does it usually survive forced colors when a \u003Ccode>&lt;div&gt;\u003C\u002Fcode> styled to look like a button doesn&#39;t?\u003C\u002Fp>",{"id":171,"html":172,"text":173,"type":136,"level":31},"b15","Why the real button survived and the fake one didn&#39;t","Why the real button survived and the fake one didn't",{"id":175,"html":176,"type":125},"b16","\u003Cp>The difference isn&#39;t that \u003Ccode>&lt;button&gt;\u003C\u002Fcode> is magic. It&#39;s that most &quot;reset&quot; button styles only remove the parts they&#39;re actively replacing, and browsers ship a default border on form controls in their user-agent stylesheet. Unless you&#39;ve specifically zeroed that out, there&#39;s still \u003Cem>a border\u003C\u002Fem> on a native \u003Ccode>&lt;button&gt;\u003C\u002Fcode> for forced colors to recolor into something visible. A \u003Ccode>&lt;div&gt;\u003C\u002Fcode> built from scratch as a custom checkbox or button usually never had a border at all — its shape existed only in the properties forced colors throws away.\u003C\u002Fp>",{"id":178,"html":179,"type":125},"b17","\u003Cp>This is a known enough pattern that there&#39;s a standing trick for it: give decorative edges a \u003Ccode>border: 1px solid transparent\u003C\u002Fcode> instead of \u003Ccode>border: none\u003C\u002Fcode> in your normal styles. Visually, nothing changes — a transparent border is invisible either way. But forced colors mode remaps border \u003Cem>colors\u003C\u002Fem>, including \u003Ccode>transparent\u003C\u002Fcode>, onto the system palette. You get a free, correctly colored boundary in forced-colors mode without touching how the component looks the other 99% of the time anyone views it.\u003C\u002Fp>",{"id":181,"html":182,"text":182,"type":136,"level":31},"b18","The actual fix",{"id":184,"html":185,"type":125},"b19","\u003Cp>The reliable fix isn&#39;t fighting the override — it&#39;s meeting it, explicitly, with the same palette the browser is already using. System color keywords like \u003Ccode>ButtonBorder\u003C\u002Fcode>, \u003Ccode>ButtonFace\u003C\u002Fcode>, \u003Ccode>Highlight\u003C\u002Fcode>, and \u003Ccode>CanvasText\u003C\u002Fcode> are valid CSS values any time, and forced colors mode treats them as already correct, so it leaves them alone instead of remapping them to something else:\u003C\u002Fp>",{"id":187,"code":188,"type":156,"language":62,"highlight":189},"b20",".custom-checkbox {\n  background: #2563eb;\n  box-shadow: 0 0 0 2px #93c5fd;\n  border: none;\n}\n\n@media (forced-colors: active) {\n  .custom-checkbox {\n    \u002F* box-shadow is gone here no matter what — give it a real border instead *\u002F\n    border: 1px solid ButtonBorder;\n    background: ButtonFace;\n  }\n\n  .custom-checkbox[aria-checked=\"true\"] {\n    background: Highlight;\n    border-color: Highlight;\n  }\n}",[],{"id":191,"html":192,"type":125},"b21","\u003Cp>Nothing in the normal-mode styles changes. Inside the media query, the checkbox gets an explicit border using a keyword that already means &quot;the border color controls should have,&quot; and its checked state uses \u003Ccode>Highlight\u003C\u002Fcode>, the same system color the browser uses for selected text and active UI. The result matches how every native control on that user&#39;s machine already looks — which is the entire point of the feature.\u003C\u002Fp>",{"id":194,"html":195,"type":125},"b22","\u003Cp>There&#39;s one more lever worth knowing, and it&#39;s a trap if you reach for it too fast: \u003Ccode>forced-color-adjust: none\u003C\u002Fcode> opts an element out of all of this, so your original colors render exactly as written, forced colors mode or not — in browsers that support the property. Unlike \u003Ccode>forced-colors\u003C\u002Fcode> itself, \u003Ccode>forced-color-adjust\u003C\u002Fcode> isn&#39;t Baseline yet: Chrome and Firefox support it, but Safari doesn&#39;t, as of this writing — so don&#39;t treat it as a guaranteed universal escape hatch, and still test the element without it. It&#39;s tempting to slap it on the whole page the moment you see something break. Don&#39;t. The moment you opt out, \u003Cem>you\u003C\u002Fem> own the contrast and visibility guarantees the user just told their OS they need — that&#39;s appropriate for a brand logo or a purely decorative illustration, and actively harmful on real text or an interactive control. If you ever use it, use it narrowly, and never on anything the user has to read or click.\u003C\u002Fp>",{"id":197,"html":198,"text":198,"type":136,"level":31},"b23","Test it before a bug report does",{"id":200,"html":201,"type":125},"b24","\u003Cp>The quickest way to see all of this without touching a single Windows setting: Chromium-based DevTools (Chrome, Edge) can emulate forced colors mode directly — open the Rendering tab and look for the CSS media feature emulation options, flip forced-colors to &quot;active,&quot; and reload. Every property swap in this post happens live, on your own page, in your own browser.\u003C\u002Fp>",{"id":203,"html":204,"type":125},"b25","\u003C!-- playground:start -->",{"id":206,"html":207,"text":207,"type":136,"level":31},"b26","🎮 Try it yourself",{"id":209,"html":210,"type":125},"b27","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fforced-colors-high-contrast-mode\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":212,"html":213,"type":125},"b28","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":215,"html":216,"type":125},"b29","\u003C!-- playground:end -->",{"id":218,"html":219,"text":219,"type":136,"level":31},"b30","What I actually changed",{"id":221,"html":222,"type":125},"b31","\u003Cp>I didn&#39;t rewrite the design system. I added one \u003Ccode>@media (forced-colors: active)\u003C\u002Fcode> block per component that relied on shadows or background color for its shape, each one just a border and maybe a background swap using system keywords. The checkout button that started this whole thing now has four extra lines of CSS. It&#39;s visible to every High Contrast user it was invisible to before, and it looks exactly the same to everyone else.\u003C\u002Fp>",{"id":224,"html":225,"type":125},"b32","\u003Cp>The lesson that actually stuck with me isn&#39;t about this one bug. It&#39;s that &quot;I tested it and it looks fine&quot; has a hidden asterisk: fine in \u003Cem>whose\u003C\u002Fem> color scheme. Dark mode earned a place in every component checklist a few years ago. Forced colors deserves the same five-second check before you call something done — because the first time you hear about it, like me, it&#39;ll be from someone who can&#39;t see your product at all.\u003C\u002Fp>",{"id":227,"html":228,"type":125},"b33","\u003C!-- quiz:start -->",{"id":230,"html":231,"text":231,"type":136,"level":31},"b34","🧠 Test yourself",{"id":233,"html":234,"type":125},"b35","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fforced-colors-high-contrast-mode\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":236,"html":237,"type":125},"b36","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":239,"html":240,"type":125},"b37","\u003C!-- quiz:end -->",{"id":242,"html":243,"type":125},"b38","\u003Cp>Have you checked your own components in forced-colors mode yet? Open DevTools, flip the emulation on, and see what quietly vanishes — I&#39;d genuinely like to know what you find.\u003C\u002Fp>",{"id":245,"type":246},"b39","divider",{"id":248,"html":249,"type":125},"b40","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":251,"html":252,"type":125},"b41","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":254,"type":255,"items":256,"ordered":18},"b42","list",[257,258,259],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","A QA ticket landed with a screenshot: \"Submit button missing on checkout.\" I opened the page in Chrome. The button was right there — blue, rounded, impossible to miss. I opened it in Firefox. Same button. I assumed a bad screenshot and closed the ticket.\n\nThree days later it came back, same screenshot, same page, from a different tester. This time I asked what they'd changed. \"Nothing,\" they said. \"I just use Windows with High Contrast on, because I'm light-sensitive.\" I flipped that setting on my own machine for the first time in years, reloaded the page, and watched my button disappear into the background like it had never existed.\n\nIt wasn't a rendering bug. It wasn't a z-index fight. It was a CSS feature I'd never once had to think about, quietly rewriting my entire color palette out from under me.\n\n## The fix that didn't fix anything\n\nMy first guess was contrast. The button used `background: #2563eb` on a white card — readable, passed every automated contrast checker I ran. So I assumed High Contrast Mode was some kind of amplifier: it probably just needed *more* contrast to work with. I darkened the blue, added a heavier `box-shadow` for depth, bumped the border a shade, and shipped it.\n\nThe next build, the button was still gone.\n\nThat's the part that should have told me something was off: I hadn't made the colors *fail* a contrast check, I'd made them *more* correct by every metric I knew how to check — and the bug didn't budge. If contrast were the problem, a darker blue should have helped. It changed nothing, because the browser was never reading my blue in the first place.\n\n## What `forced-colors` actually does\n\nWindows High Contrast Mode doesn't ask the browser to render your page with better contrast. It tells the browser to render your page using the operating system's own fixed palette — full stop, author CSS or not. Every modern browser exposes this as a media feature:\n\n```css\n@media (forced-colors: active) {\n  \u002F* the user has an OS-level forced-colors palette turned on *\u002F\n}\n```\n\nIt only ever matches when the *user* has flipped an OS-level switch (Windows High Contrast themes are the classic trigger) — you can't turn it on with JavaScript, and `prefers-contrast` is a different, much gentler feature that just reports a preference for more or less contrast without touching your styles at all. `forced-colors` is not a hint. When it's active, the browser overrides nearly every color-related property you wrote — `color`, `background-color`, `border-color`, `outline-color` — and remaps each one onto one of a couple dozen system colors, chosen by what kind of element it's on. Normal text becomes `CanvasText` on a `Canvas` background. A button's label becomes `ButtonText`. None of that is something your stylesheet gets a vote on.\n\nTwo more things get treated even more bluntly: `box-shadow` and `text-shadow` are forced to `none`, and any `background-image` that isn't a `url()` is forced to `none` too. There's no sensible way to remap a drop shadow or a gradient onto a small fixed palette, so the browser doesn't try — it just removes them.\n\nHere's the part that explains my exact bug. My button's shape came from `background: #2563eb` plus a `box-shadow` for depth, and it had `border: none`. Under forced colors: the background got remapped to some flat system color, the shadow vanished outright, and because I'd never declared an actual border, there was nothing left for the browser to recolor into a visible edge. The button didn't get *uglier* — it got structurally invisible, blending straight into the page.\n\nQuick gut check before you keep reading: if a native `\u003Cbutton>` has the exact same styles — `background`, `box-shadow`, `border: none` — why does it usually survive forced colors when a `\u003Cdiv>` styled to look like a button doesn't?\n\n## Why the real button survived and the fake one didn't\n\nThe difference isn't that `\u003Cbutton>` is magic. It's that most \"reset\" button styles only remove the parts they're actively replacing, and browsers ship a default border on form controls in their user-agent stylesheet. Unless you've specifically zeroed that out, there's still *a border* on a native `\u003Cbutton>` for forced colors to recolor into something visible. A `\u003Cdiv>` built from scratch as a custom checkbox or button usually never had a border at all — its shape existed only in the properties forced colors throws away.\n\nThis is a known enough pattern that there's a standing trick for it: give decorative edges a `border: 1px solid transparent` instead of `border: none` in your normal styles. Visually, nothing changes — a transparent border is invisible either way. But forced colors mode remaps border *colors*, including `transparent`, onto the system palette. You get a free, correctly colored boundary in forced-colors mode without touching how the component looks the other 99% of the time anyone views it.\n\n## The actual fix\n\nThe reliable fix isn't fighting the override — it's meeting it, explicitly, with the same palette the browser is already using. System color keywords like `ButtonBorder`, `ButtonFace`, `Highlight`, and `CanvasText` are valid CSS values any time, and forced colors mode treats them as already correct, so it leaves them alone instead of remapping them to something else:\n\n```css\n.custom-checkbox {\n  background: #2563eb;\n  box-shadow: 0 0 0 2px #93c5fd;\n  border: none;\n}\n\n@media (forced-colors: active) {\n  .custom-checkbox {\n    \u002F* box-shadow is gone here no matter what — give it a real border instead *\u002F\n    border: 1px solid ButtonBorder;\n    background: ButtonFace;\n  }\n\n  .custom-checkbox[aria-checked=\"true\"] {\n    background: Highlight;\n    border-color: Highlight;\n  }\n}\n```\n\nNothing in the normal-mode styles changes. Inside the media query, the checkbox gets an explicit border using a keyword that already means \"the border color controls should have,\" and its checked state uses `Highlight`, the same system color the browser uses for selected text and active UI. The result matches how every native control on that user's machine already looks — which is the entire point of the feature.\n\nThere's one more lever worth knowing, and it's a trap if you reach for it too fast: `forced-color-adjust: none` opts an element out of all of this, so your original colors render exactly as written, forced colors mode or not — in browsers that support the property. Unlike `forced-colors` itself, `forced-color-adjust` isn't Baseline yet: Chrome and Firefox support it, but Safari doesn't, as of this writing — so don't treat it as a guaranteed universal escape hatch, and still test the element without it. It's tempting to slap it on the whole page the moment you see something break. Don't. The moment you opt out, *you* own the contrast and visibility guarantees the user just told their OS they need — that's appropriate for a brand logo or a purely decorative illustration, and actively harmful on real text or an interactive control. If you ever use it, use it narrowly, and never on anything the user has to read or click.\n\n## Test it before a bug report does\n\nThe quickest way to see all of this without touching a single Windows setting: Chromium-based DevTools (Chrome, Edge) can emulate forced colors mode directly — open the Rendering tab and look for the CSS media feature emulation options, flip forced-colors to \"active,\" and reload. Every property swap in this post happens live, on your own page, in your own browser.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fforced-colors-high-contrast-mode\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## What I actually changed\n\nI didn't rewrite the design system. I added one `@media (forced-colors: active)` block per component that relied on shadows or background color for its shape, each one just a border and maybe a background swap using system keywords. The checkout button that started this whole thing now has four extra lines of CSS. It's visible to every High Contrast user it was invisible to before, and it looks exactly the same to everyone else.\n\nThe lesson that actually stuck with me isn't about this one bug. It's that \"I tested it and it looks fine\" has a hidden asterisk: fine in *whose* color scheme. Dark mode earned a place in every component checklist a few years ago. Forced colors deserves the same five-second check before you call something done — because the first time you hear about it, like me, it'll be from someone who can't see your product at all.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fforced-colors-high-contrast-mode\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nHave you checked your own components in forced-colors mode yet? Open DevTools, flip the emulation on, and see what quietly vanishes — I'd genuinely like to know what you find.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":96,"canonical":262,"description":263},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fforced-colors-high-contrast-mode","Windows High Contrast Mode doesn't just boost contrast — it strips your backgrounds, shadows, and colors outright. Here's what `forced-colors` actually does, why a styled \u003Cdiv> van","01a133e2-3e8a-740e-97f9-30234de82dfd",{"id":266,"locked":18},"01a133e2-3eae-7559-8039-094597576557",[268],{"id":269,"slug":95,"title":270,"_count":271},"01a133e2-3ecc-74b1-9a15-3c7aaba01b39","forced-colors mode",{"questions":100},[273],{"locale":13,"slug":95},{"id":269,"slug":95,"title":270,"_count":275,"questionCount":100},{"questions":100},[],{"items":278,"meta":406},[279,291,310,329,347,367,387],{"id":94,"slug":95,"title":96,"subtitle":97,"excerpt":98,"coverUrl":99,"locale":13,"readingMinutes":100,"publishedAt":101,"viewCount":280,"likeCount":19,"commentCount":19,"author":281,"vertical":282,"topic":283,"tags":284,"_count":289,"playground":290,"hasQuiz":17,"hasPlayground":17},28,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},[285,286,287,288],{"slug":62,"name":63,"color":97},{"slug":90,"name":91,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"assessments":117},{"slug":95},{"id":292,"slug":293,"title":294,"subtitle":97,"excerpt":295,"coverUrl":296,"locale":13,"readingMinutes":297,"publishedAt":298,"viewCount":299,"likeCount":19,"commentCount":19,"author":300,"vertical":301,"topic":302,"tags":303,"_count":308,"playground":309,"hasQuiz":17,"hasPlayground":17},"01a12aed-3621-77cf-8827-f0fa7f687105","css-contain-rendering-isolation","Stop Letting One Widget Reflow the Whole Page. `contain` Scopes It.","A live counter or chat widget shouldn't make the rest of your page recompute layout every time it updates. The CSS contain property draws a hard boundary the browser can actually trust — here's what it promises, and the one gotcha that breaks pages when people misuse it.","\u002Fmedia\u002Fcovers\u002Fcss-contain-rendering-isolation.png",6,"2026-10-16T09:23:09.005Z",125,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},[304,305,306,307],{"slug":62,"name":63,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"assessments":117},{"slug":293},{"id":311,"slug":312,"title":313,"subtitle":97,"excerpt":314,"coverUrl":315,"locale":13,"readingMinutes":316,"publishedAt":317,"viewCount":318,"likeCount":19,"commentCount":19,"author":319,"vertical":320,"topic":321,"tags":322,"_count":327,"playground":328,"hasQuiz":17,"hasPlayground":17},"01a10aaf-5ec4-700b-bbd2-5fc17c27d4e0","css-will-change-performance-hint","You Added `will-change` to Fix the Jank. You Made It Worse.","will-change: transform looks like a free performance switch. Applied broadly or left on permanently, it does the opposite — here's the narrow, temporary pattern that actually works.","\u002Fmedia\u002Fcovers\u002Fcss-will-change-performance-hint.png",5,"2026-10-10T12:12:00.282Z",219,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},[323,324,325,326],{"slug":62,"name":63,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"assessments":117},{"slug":312},{"id":330,"slug":331,"title":332,"subtitle":97,"excerpt":333,"coverUrl":334,"locale":13,"readingMinutes":297,"publishedAt":335,"viewCount":336,"likeCount":19,"commentCount":19,"author":337,"vertical":338,"topic":339,"tags":340,"_count":345,"playground":346,"hasQuiz":17,"hasPlayground":17},"01a106cb-f870-70ac-892c-2f9a55db26b3","prefers-reduced-motion-respect-the-setting","You're Animating for Everyone. `prefers-reduced-motion` Fixes That.","Most animated UIs never check whether the visitor asked for less motion. `prefers-reduced-motion` is the CSS media feature that tells you — and a fail-closed pattern keeps you honest even when a browser doesn't support it.","\u002Fmedia\u002Fcovers\u002Fprefers-reduced-motion-respect-the-setting.png","2026-10-09T06:10:28.564Z",436,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},[341,342,343,344],{"slug":62,"name":63,"color":97},{"slug":90,"name":91,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"assessments":117},{"slug":331},{"id":348,"slug":349,"title":350,"subtitle":97,"excerpt":351,"coverUrl":352,"locale":13,"readingMinutes":297,"publishedAt":353,"viewCount":336,"likeCount":19,"commentCount":19,"author":354,"vertical":355,"topic":356,"tags":359,"_count":365,"playground":366,"hasQuiz":17,"hasPlayground":17},"01a0ec73-289b-72dc-b408-cb3deb2a38a9","details-name-exclusive-accordion","Stop Hand-Rolling an Exclusive Accordion. `\u003Cdetails name>` Does It","You've probably shipped an accordion with a hand-rolled 'close the other panels' loop in JavaScript. The `name` attribute on \u003Cdetails> does it natively — no JS, no ARIA plumbing.","\u002Fmedia\u002Fcovers\u002Fdetails-name-exclusive-accordion.png","2026-10-04T12:03:17.379Z",{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":357,"name":358},"html","HTML",[360,362,363,364],{"slug":357,"name":361,"color":97},"Html",{"slug":62,"name":63,"color":97},{"slug":46,"name":47,"color":97},{"slug":90,"name":91,"color":97},{"assessments":117},{"slug":349},{"id":368,"slug":369,"title":370,"subtitle":97,"excerpt":371,"coverUrl":372,"locale":13,"readingMinutes":316,"publishedAt":373,"viewCount":374,"likeCount":19,"commentCount":19,"author":375,"vertical":376,"topic":377,"tags":378,"_count":385,"playground":386,"hasQuiz":17,"hasPlayground":17},"01a0ec73-1ead-73da-8d85-b6b797fafb03","css-text-box-trim-headline-gap","Stop Fixing Headline Gaps With Negative Margins. Use `text-box-trim`","Line-height reserves space for accent marks your headline never uses — a hand-tuned negative margin only fixes it for one font at one size. text-box-trim does, natively, from the font's own metrics.","\u002Fmedia\u002Fcovers\u002Fcss-text-box-trim-headline-gap.png","2026-10-02T06:10:31.028Z",393,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},[379,380,381,382],{"slug":62,"name":63,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"slug":383,"name":384,"color":97},"typography","Typography",{"assessments":117},{"slug":369},{"id":388,"slug":389,"title":390,"subtitle":97,"excerpt":391,"coverUrl":392,"locale":13,"readingMinutes":393,"publishedAt":394,"viewCount":395,"likeCount":19,"commentCount":19,"author":396,"vertical":397,"topic":398,"tags":399,"_count":404,"playground":405,"hasQuiz":17,"hasPlayground":17},"01a0ec73-195e-713c-b27c-9beace8ef7df","nth-child-of-selector-list","`:nth-child(even)` Counts Hidden Rows. `of S` Fixes Your Stripes","nth-child(even) counts every element in the DOM, including the ones you just hid with display:none. The fix isn't JavaScript — it's a CSS selector most people have never had a reason to use.","\u002Fmedia\u002Fcovers\u002Fnth-child-of-selector-list.png",4,"2026-10-01T06:10:26.375Z",371,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":62,"name":110},[400,401,402,403],{"slug":62,"name":63,"color":97},{"slug":46,"name":47,"color":97},{"slug":54,"name":55,"color":97},{"slug":58,"name":59,"color":97},{"assessments":117},{"slug":389},{"page":117,"perPage":100,"total":64,"totalPages":100},"\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">@media\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">forced-colors\u003C\u002Fspan>: \u003Cspan class=\"shj-kwd\">active\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F* the user has an OS-level forced-colors palette turned on *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">.custom-checkbox\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">background\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#2563eb\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">box-shadow\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-num\">2\u003C\u002Fspan>\u003Cspan class=\"shj-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-num\">#93c5fd\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">border\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">none\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">@media\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">forced-colors\u003C\u002Fspan>: \u003Cspan class=\"shj-kwd\">active\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">.custom-checkbox\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-cmnt\">\u002F* box-shadow is gone here no matter what — give it a real border instead *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">border\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-num\">solid\u003C\u002Fspan> ButtonBorder;\n    \u003Cspan class=\"shj-var\">background\u003C\u002Fspan>: ButtonFace;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-var\">.custom-checkbox\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">aria-checked\u003C\u002Fspan>=\u003Cspan class=\"shj-str\">\"true\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">background\u003C\u002Fspan>: Highlight;\n    \u003Cspan class=\"shj-var\">border-color\u003C\u002Fspan>: Highlight;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":410},[]]