[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-css-relative-color-syntax":44,"search-suggestions":251,"path-membership-css-relative-color-syntax":291,"related-css-relative-color-syntax":292,"code:css:true:jvmzi2":423,"code:css:true:124dmlq":424,"code:css:true:3jszqn":425,"code:css:true:13exv11":426,"comments-01a176b8-6fee-705e-9118-a40e4feb9312":427},[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,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":235,"seo":236,"translationGroupId":239,"series":48,"podcastUrl":48,"verticalId":5,"thread":240,"assessments":242,"translations":247,"quiz":249},"01a176b8-6fee-705e-9118-a40e4feb9312","css-relative-color-syntax","Stop Hardcoding Color Variants. Relative Color Syntax Does It.",null,"A design system ships one brand color and four hand-picked hex variants for hover, pressed, border, and tint. Change the brand color and four of those five silently go stale. CSS relative color syntax derives every variant from the one color you actually update.","\u002Fmedia\u002Fcovers\u002Fcss-relative-color-syntax.png",7,"2026-11-01T14:51:27.260Z",31,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"css","CSS",[64,66,69,72],{"slug":61,"name":65,"color":48},"Css",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"frontend","Frontend",{"slug":73,"name":74,"color":48},"programming","Programming",{"assessments":76},1,{"slug":46,"title":78},"CSS relative color syntax — interactive playground",{"blocks":80,"version":76},[81,85,88,92,95,100,103,106,110,113,116,119,123,126,130,133,136,139,143,146,149,152,155,158,161,164,167,170,174,177,180,183,186,189,192,195,198,201,204,207,210,217,220,223,226,229],{"id":82,"html":83,"type":84},"b1","\u003Cp>The PR comment was two words: &quot;wrong blue.&quot; Not a bug report, not a reproduction — a designer scrolling through a staging build, stopping on the &quot;Upgrade&quot; button&#39;s hover state, and flagging it because something about it felt off against the rest of the rebrand that had shipped two weeks earlier.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>They were right, and it took longer than it should have to find out why. The button&#39;s resting color matched the new brand blue exactly — that token had been updated the day the rebrand landed. Its hover color was still the \u003Cem>old\u003C\u002Fem> brand blue, darkened. Nobody had touched it, because nobody remembered it existed.\u003C\u002Fp>",{"id":89,"html":90,"text":90,"type":91,"level":31},"b3","Five variables, one of which actually gets updated","heading",{"id":93,"html":94,"type":84},"b4","\u003Cp>Open almost any design system&#39;s color file and you&#39;ll find the same shape: one brand color, and a handful of hand-picked variants sitting next to it that all \u003Cem>look\u003C\u002Fem> derived but are actually just... more hex codes.\u003C\u002Fp>",{"id":96,"code":97,"type":98,"language":61,"highlight":99},"b5",":root {\n  --brand: #3b6fd6;\n  --brand-hover: #2f58ab;      \u002F* \"a bit darker\" — picked by eye, once *\u002F\n  --brand-active: #244289;     \u002F* \"darker still\" — same deal *\u002F\n  --brand-border: #6e92e1;     \u002F* \"a bit lighter\" *\u002F\n  --brand-tint: rgba(59, 111, 214, 0.08); \u002F* \"faint background wash\" *\u002F\n}","code",[],{"id":101,"html":102,"type":84},"b6","\u003Cp>Every one of those four extra values is somebody&#39;s good-faith attempt, at some point in the past, to eyeball a lighter or darker or more-transparent version of \u003Ccode>--brand\u003C\u002Fcode>. They&#39;re not wrong on day one. The problem shows up the day \u003Ccode>--brand\u003C\u002Fcode> changes and the other four don&#39;t — because CSS has no idea they were ever related. To the engine, \u003Ccode>--brand-hover\u003C\u002Fcode> isn&#39;t &quot;darker \u003Ccode>--brand\u003C\u002Fcode>.&quot; It&#39;s just a string that starts with \u003Ccode>#2f58ab\u003C\u002Fcode>, full stop.\u003C\u002Fp>",{"id":104,"html":105,"type":84},"b7","\u003Cp>The &quot;obvious&quot; fix is discipline: a comment reminding whoever edits \u003Ccode>--brand\u003C\u002Fcode> to also update the other four, or a design-tokens build step that regenerates them from a single source before CSS ever sees them. Both work, right up until the person making a one-line brand tweak six months from now doesn&#39;t know the build step exists, or skims past the comment. Discipline that depends on nobody being in a hurry isn&#39;t a fix — it&#39;s a bet, and the rebrand PR is the bet losing.\u003C\u002Fp>",{"id":107,"html":108,"text":109,"type":91,"level":31},"b8","The variables were never the problem — storing the \u003Cem>relationship\u003C\u002Fem> was","The variables were never the problem — storing the relationship was",{"id":111,"html":112,"type":84},"b9","\u003Cp>Here&#39;s the reframe that matters: \u003Ccode>--brand-hover\u003C\u002Fcode> was never really an independent color. It was always a \u003Cem>function of\u003C\u002Fem> \u003Ccode>--brand\u003C\u002Fcode> — &quot;darker, same hue.&quot; \u003Ccode>--brand-tint\u003C\u002Fcode> was always &quot;the same color, 8% opaque.&quot; The relationship was real and well understood by whoever first picked those hex codes. What got lost was that the relationship lived in a person&#39;s head for thirty seconds and then evaporated, leaving only its one-time output behind, frozen in a stylesheet forever.\u003C\u002Fp>",{"id":114,"html":115,"type":84},"b10","\u003Cp>What you actually want is for CSS to store the \u003Cem>relationship\u003C\u002Fem>, not just a snapshot of what it once produced — and that&#39;s a genuinely new category of thing for CSS to express. For most of CSS&#39;s life, a color value was a dead end: \u003Ccode>#3b6fd6\u003C\u002Fcode> tells the renderer what to paint, but nothing about where it came from or how to get a cousin of it. \u003Ccode>color-mix()\u003C\u002Fcode> loosened that slightly — you can blend two colors — but blending two colors isn&#39;t the shape of this problem. You don&#39;t have two brand colors to mix; you have \u003Cstrong>one\u003C\u002Fstrong> color, and you want its lightness turned down a notch while everything else about it stays put.\u003C\u002Fp>",{"id":117,"html":118,"type":84},"b11","\u003Cp>\u003Cstrong>Before you keep reading: what would you even call the CSS feature that lets you say &quot;same color, just darker&quot;?\u003C\u002Fstrong> It&#39;s a fair question, because until relatively recently there wasn&#39;t a clean answer.\u003C\u002Fp>",{"id":120,"html":121,"text":122,"type":91,"level":31},"b12","Relative color syntax: read a color&#39;s own channels back out","Relative color syntax: read a color's own channels back out",{"id":124,"html":125,"type":84},"b13","\u003Cp>Relative color syntax lets a color function take another color as input and expose \u003Cem>that color&#39;s own channels\u003C\u002Fem> as variables you can do math on, right there in the function call:\u003C\u002Fp>",{"id":127,"code":128,"type":98,"language":61,"highlight":129},"b14",":root {\n  --brand: #3b6fd6;\n\n  \u002F* \"same hue and chroma as --brand, just darker\" *\u002F\n  --brand-hover:  oklch(from var(--brand) calc(l - 0.12) c h);\n  --brand-active: oklch(from var(--brand) calc(l - 0.20) c h);\n\n  \u002F* \"same color, lighter, for a border\" *\u002F\n  --brand-border: oklch(from var(--brand) calc(l + 0.15) c h);\n\n  \u002F* \"same color, 8% opaque\" *\u002F\n  --brand-tint: oklch(from var(--brand) l c h \u002F 8%);\n}",[],{"id":131,"html":132,"type":84},"b15","\u003Cp>Read the \u003Ccode>oklch(from var(--brand) ...)\u003C\u002Fcode> part literally: &quot;take \u003Ccode>--brand\u003C\u002Fcode>, convert it to OKLCH, and give me three variables back — \u003Ccode>l\u003C\u002Fcode> for lightness, \u003Ccode>c\u003C\u002Fcode> for chroma, \u003Ccode>h\u003C\u002Fcode> for hue.&quot; Whatever you write after that is just CSS math on those three numbers (plus an optional alpha after the slash). \u003Ccode>calc(l - 0.12)\u003C\u002Fcode> means &quot;this color&#39;s own lightness, minus 0.12&quot; — not a hardcoded lightness value, a relationship to whatever \u003Ccode>--brand\u003C\u002Fcode> happens to be \u003Cem>right now\u003C\u002Fem>.\u003C\u002Fp>",{"id":134,"html":135,"type":84},"b16","\u003Cp>Change \u003Ccode>--brand\u003C\u002Fcode> to a rebrand&#39;s new blue, or swap it entirely for a dark-mode theme, and every line below it recomputes. There&#39;s no second hex code to forget, because there is no second hex code — \u003Ccode>--brand-hover\u003C\u002Fcode> isn&#39;t stored anywhere; it&#39;s derived, every time the browser needs it, from the one variable a human actually edits.\u003C\u002Fp>",{"id":137,"html":138,"type":84},"b17","\u003Cp>This isn&#39;t OKLCH-only, either. \u003Ccode>rgb()\u003C\u002Fcode>, \u003Ccode>hsl()\u003C\u002Fcode>, and \u003Ccode>color()\u003C\u002Fcode> all support the same \u003Ccode>from\u003C\u002Fcode> syntax, exposing whichever channels that color space naturally has:\u003C\u002Fp>",{"id":140,"code":141,"type":98,"language":61,"highlight":142},"b18","\u002F* rgb()'s own channels: r, g, b (0–255), alpha *\u002F\n.badge { background: rgb(from var(--brand) r g b \u002F 15%); }\n\n\u002F* hsl()'s own channels: h, s, l, alpha — s and l come back as plain\n   numbers (0-100), not percentages, so the math below uses 10, not 10% *\u002F\n.link:hover { color: hsl(from var(--brand) h s calc(l - 10)); }",[],{"id":144,"html":145,"type":84},"b19","\u003Cp>OKLCH is usually the better pick for lightness math specifically, because it&#39;s designed so that &quot;lightness&quot; tracks \u003Cem>perceived\u003C\u002Fem> brightness consistently across hues — nudging \u003Ccode>l\u003C\u002Fcode> down by the same amount darkens a yellow and a blue by roughly the same perceived amount. Nudge \u003Ccode>hsl()\u003C\u002Fcode>&#39;s \u003Ccode>l\u003C\u002Fcode> channel the same way and the result can feel inconsistent hue to hue, because HSL&#39;s lightness was never built to model human perception in the first place.\u003C\u002Fp>",{"id":147,"html":148,"type":84},"b20","\u003C!-- playground:start -->",{"id":150,"html":151,"text":151,"type":91,"level":31},"b21","🎮 Try it yourself",{"id":153,"html":154,"type":84},"b22","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-relative-color-syntax\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":156,"html":157,"type":84},"b23","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":159,"html":160,"type":84},"b24","\u003C!-- playground:end -->",{"id":162,"html":163,"text":163,"type":91,"level":31},"b25","The one sharp edge: clamp your math",{"id":165,"html":166,"type":84},"b26","\u003Cp>\u003Ccode>calc(l - 0.12)\u003C\u002Fcode> is honest math, which means it&#39;ll happily produce a number outside the range that makes sense. Pick a brand color that&#39;s already quite dark, subtract 0.20 from its lightness, and \u003Ccode>l\u003C\u002Fcode> can go negative — which isn&#39;t a color error so much as a &quot;this doesn&#39;t mean anything&quot; result, rendered as whatever the browser decides black looks like.\u003C\u002Fp>",{"id":168,"html":169,"type":84},"b27","\u003Cp>The fix is the same \u003Ccode>clamp()\u003C\u002Fcode> you&#39;d reach for anywhere else in CSS, wrapped around the channel math:\u003C\u002Fp>",{"id":171,"code":172,"type":98,"language":61,"highlight":173},"b28","--brand-hover: oklch(from var(--brand) clamp(0, calc(l - 0.12), 1) c h);",[],{"id":175,"html":176,"type":84},"b29","\u003Cp>That one change makes the derivation safe for \u003Cem>any\u003C\u002Fem> input color, including ones your design system doesn&#39;t control yet — a user-customizable accent color, say, or a brand color read from a CMS. Skip the clamp and the formula only works for colors in roughly the lightness range you tested it against, which defeats a good chunk of the point: a derivation that only works for some inputs is just a more elaborate hardcoded value.\u003C\u002Fp>",{"id":178,"html":179,"text":179,"type":91,"level":31},"b30","Is this safe to ship, or is it another flag-gated preview?",{"id":181,"html":182,"type":84},"b31","\u003Cp>Safe to ship — but the rollout order isn&#39;t what you&#39;d guess. Safari 16.4 (March 2023) was the earliest browser with \u003Cem>something\u003C\u002Fem>, but it was a partial pass at an older draft of the spec with real parsing bugs; that implementation was later replaced, not extended. Chrome and Firefox did the work of actually matching the final spec through 2024 (Firefox in Firefox 128, July 2024), and Safari didn&#39;t catch up — bug fixes included — until Safari 18 in September 2024. So for the exact forms used above, Safari was last to ship the behavior this post describes, not first. Either way, that was over two years ago: by now every major evergreen browser has unprefixed, unflagged, spec-correct support, and there&#39;s no realistic &quot;but what about X&quot; objection left to raise for a typical modern browser matrix. If your project still has to support browsers from before that window, the fallback is simple and honest: keep writing literal hex variants for those browsers, and layer the relative syntax on top for everyone else — it degrades to &quot;the hardcoded value you already had,&quot; not to a broken page.\u003C\u002Fp>",{"id":184,"html":185,"type":84},"b32","\u003C!-- quiz:start -->",{"id":187,"html":188,"text":188,"type":91,"level":31},"b33","🧠 Test yourself",{"id":190,"html":191,"type":84},"b34","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-relative-color-syntax\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":193,"html":194,"type":84},"b35","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":196,"html":197,"type":84},"b36","\u003C!-- quiz:end -->",{"id":199,"html":200,"type":84},"b37","\u003Cp>The bug wasn&#39;t really &quot;someone forgot to update a hex code.&quot; It was that the stylesheet never had anywhere to \u003Cem>put\u003C\u002Fem> the fact that \u003Ccode>--brand-hover\u003C\u002Fcode> meant &quot;darker \u003Ccode>--brand\u003C\u002Fcode>&quot; — so that knowledge lived in a person&#39;s memory instead, with exactly the shelf life you&#39;d expect. Relative color syntax isn&#39;t a shorter way to write the same five hex codes. It&#39;s a way to stop writing four of them at all.\u003C\u002Fp>",{"id":202,"html":203,"type":84},"b38","\u003Cp>Where does your own color system still keep that kind of relationship in a comment instead of in the CSS itself?\u003C\u002Fp>",{"id":205,"html":206,"type":84},"b39","\u003C!-- related:start -->",{"id":208,"html":209,"text":209,"type":91,"level":31},"b40","📚 Read next",{"id":211,"type":212,"items":213,"ordered":18},"b41","list",[214,215,216],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-color-scheme-native-dark-mode-form-controls\">The CSS Property That Fixes Dark Mode&#39;s Ugly White Boxes\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-accent-color-native-form-control-tinting\">One CSS Property Replaces Your Checkbox Hack\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-color-mix-native-color-mixing\">You reach for Sass to mix colors. \u003Ccode>color-mix()\u003C\u002Fcode> does it natively.\u003C\u002Fa>",{"id":218,"html":219,"type":84},"b42","\u003C!-- related:end -->",{"id":221,"type":222},"b43","divider",{"id":224,"html":225,"type":84},"b44","\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":227,"html":228,"type":84},"b45","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":230,"type":212,"items":231,"ordered":18},"b46",[232,233,234],"⭐ \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>","The PR comment was two words: \"wrong blue.\" Not a bug report, not a reproduction — a designer scrolling through a staging build, stopping on the \"Upgrade\" button's hover state, and flagging it because something about it felt off against the rest of the rebrand that had shipped two weeks earlier.\n\nThey were right, and it took longer than it should have to find out why. The button's resting color matched the new brand blue exactly — that token had been updated the day the rebrand landed. Its hover color was still the *old* brand blue, darkened. Nobody had touched it, because nobody remembered it existed.\n\n## Five variables, one of which actually gets updated\n\nOpen almost any design system's color file and you'll find the same shape: one brand color, and a handful of hand-picked variants sitting next to it that all *look* derived but are actually just... more hex codes.\n\n```css\n:root {\n  --brand: #3b6fd6;\n  --brand-hover: #2f58ab;      \u002F* \"a bit darker\" — picked by eye, once *\u002F\n  --brand-active: #244289;     \u002F* \"darker still\" — same deal *\u002F\n  --brand-border: #6e92e1;     \u002F* \"a bit lighter\" *\u002F\n  --brand-tint: rgba(59, 111, 214, 0.08); \u002F* \"faint background wash\" *\u002F\n}\n```\n\nEvery one of those four extra values is somebody's good-faith attempt, at some point in the past, to eyeball a lighter or darker or more-transparent version of `--brand`. They're not wrong on day one. The problem shows up the day `--brand` changes and the other four don't — because CSS has no idea they were ever related. To the engine, `--brand-hover` isn't \"darker `--brand`.\" It's just a string that starts with `#2f58ab`, full stop.\n\nThe \"obvious\" fix is discipline: a comment reminding whoever edits `--brand` to also update the other four, or a design-tokens build step that regenerates them from a single source before CSS ever sees them. Both work, right up until the person making a one-line brand tweak six months from now doesn't know the build step exists, or skims past the comment. Discipline that depends on nobody being in a hurry isn't a fix — it's a bet, and the rebrand PR is the bet losing.\n\n## The variables were never the problem — storing the *relationship* was\n\nHere's the reframe that matters: `--brand-hover` was never really an independent color. It was always a *function of* `--brand` — \"darker, same hue.\" `--brand-tint` was always \"the same color, 8% opaque.\" The relationship was real and well understood by whoever first picked those hex codes. What got lost was that the relationship lived in a person's head for thirty seconds and then evaporated, leaving only its one-time output behind, frozen in a stylesheet forever.\n\nWhat you actually want is for CSS to store the *relationship*, not just a snapshot of what it once produced — and that's a genuinely new category of thing for CSS to express. For most of CSS's life, a color value was a dead end: `#3b6fd6` tells the renderer what to paint, but nothing about where it came from or how to get a cousin of it. `color-mix()` loosened that slightly — you can blend two colors — but blending two colors isn't the shape of this problem. You don't have two brand colors to mix; you have **one** color, and you want its lightness turned down a notch while everything else about it stays put.\n\n**Before you keep reading: what would you even call the CSS feature that lets you say \"same color, just darker\"?** It's a fair question, because until relatively recently there wasn't a clean answer.\n\n## Relative color syntax: read a color's own channels back out\n\nRelative color syntax lets a color function take another color as input and expose *that color's own channels* as variables you can do math on, right there in the function call:\n\n```css\n:root {\n  --brand: #3b6fd6;\n\n  \u002F* \"same hue and chroma as --brand, just darker\" *\u002F\n  --brand-hover:  oklch(from var(--brand) calc(l - 0.12) c h);\n  --brand-active: oklch(from var(--brand) calc(l - 0.20) c h);\n\n  \u002F* \"same color, lighter, for a border\" *\u002F\n  --brand-border: oklch(from var(--brand) calc(l + 0.15) c h);\n\n  \u002F* \"same color, 8% opaque\" *\u002F\n  --brand-tint: oklch(from var(--brand) l c h \u002F 8%);\n}\n```\n\nRead the `oklch(from var(--brand) ...)` part literally: \"take `--brand`, convert it to OKLCH, and give me three variables back — `l` for lightness, `c` for chroma, `h` for hue.\" Whatever you write after that is just CSS math on those three numbers (plus an optional alpha after the slash). `calc(l - 0.12)` means \"this color's own lightness, minus 0.12\" — not a hardcoded lightness value, a relationship to whatever `--brand` happens to be *right now*.\n\nChange `--brand` to a rebrand's new blue, or swap it entirely for a dark-mode theme, and every line below it recomputes. There's no second hex code to forget, because there is no second hex code — `--brand-hover` isn't stored anywhere; it's derived, every time the browser needs it, from the one variable a human actually edits.\n\nThis isn't OKLCH-only, either. `rgb()`, `hsl()`, and `color()` all support the same `from` syntax, exposing whichever channels that color space naturally has:\n\n```css\n\u002F* rgb()'s own channels: r, g, b (0–255), alpha *\u002F\n.badge { background: rgb(from var(--brand) r g b \u002F 15%); }\n\n\u002F* hsl()'s own channels: h, s, l, alpha — s and l come back as plain\n   numbers (0-100), not percentages, so the math below uses 10, not 10% *\u002F\n.link:hover { color: hsl(from var(--brand) h s calc(l - 10)); }\n```\n\nOKLCH is usually the better pick for lightness math specifically, because it's designed so that \"lightness\" tracks *perceived* brightness consistently across hues — nudging `l` down by the same amount darkens a yellow and a blue by roughly the same perceived amount. Nudge `hsl()`'s `l` channel the same way and the result can feel inconsistent hue to hue, because HSL's lightness was never built to model human perception in the first place.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-relative-color-syntax\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The one sharp edge: clamp your math\n\n`calc(l - 0.12)` is honest math, which means it'll happily produce a number outside the range that makes sense. Pick a brand color that's already quite dark, subtract 0.20 from its lightness, and `l` can go negative — which isn't a color error so much as a \"this doesn't mean anything\" result, rendered as whatever the browser decides black looks like.\n\nThe fix is the same `clamp()` you'd reach for anywhere else in CSS, wrapped around the channel math:\n\n```css\n--brand-hover: oklch(from var(--brand) clamp(0, calc(l - 0.12), 1) c h);\n```\n\nThat one change makes the derivation safe for *any* input color, including ones your design system doesn't control yet — a user-customizable accent color, say, or a brand color read from a CMS. Skip the clamp and the formula only works for colors in roughly the lightness range you tested it against, which defeats a good chunk of the point: a derivation that only works for some inputs is just a more elaborate hardcoded value.\n\n## Is this safe to ship, or is it another flag-gated preview?\n\nSafe to ship — but the rollout order isn't what you'd guess. Safari 16.4 (March 2023) was the earliest browser with *something*, but it was a partial pass at an older draft of the spec with real parsing bugs; that implementation was later replaced, not extended. Chrome and Firefox did the work of actually matching the final spec through 2024 (Firefox in Firefox 128, July 2024), and Safari didn't catch up — bug fixes included — until Safari 18 in September 2024. So for the exact forms used above, Safari was last to ship the behavior this post describes, not first. Either way, that was over two years ago: by now every major evergreen browser has unprefixed, unflagged, spec-correct support, and there's no realistic \"but what about X\" objection left to raise for a typical modern browser matrix. If your project still has to support browsers from before that window, the fallback is simple and honest: keep writing literal hex variants for those browsers, and layer the relative syntax on top for everyone else — it degrades to \"the hardcoded value you already had,\" not to a broken page.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-relative-color-syntax\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\nThe bug wasn't really \"someone forgot to update a hex code.\" It was that the stylesheet never had anywhere to *put* the fact that `--brand-hover` meant \"darker `--brand`\" — so that knowledge lived in a person's memory instead, with exactly the shelf life you'd expect. Relative color syntax isn't a shorter way to write the same five hex codes. It's a way to stop writing four of them at all.\n\nWhere does your own color system still keep that kind of relationship in a comment instead of in the CSS itself?\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [The CSS Property That Fixes Dark Mode's Ugly White Boxes](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-color-scheme-native-dark-mode-form-controls)\n- [One CSS Property Replaces Your Checkbox Hack](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-accent-color-native-form-control-tinting)\n- [You reach for Sass to mix colors. `color-mix()` does it natively.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-color-mix-native-color-mixing)\n\n\u003C!-- related:end -->\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":47,"canonical":237,"description":238},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-relative-color-syntax","A design system ships one brand color and four hand-picked hex variants for hover, pressed, border, and tint. Change the brand color and four of those five silently go stale. CSS r","01a176b8-6fef-7107-b7f1-e5838d5de700",{"id":241,"locked":18},"01a176b8-701a-77b5-89bc-760f3d476e74",[243],{"id":244,"slug":46,"title":245,"_count":246},"01a176b8-7054-76b4-96a6-0ff60e292e09","CSS relative color syntax",{"questions":51},[248],{"locale":13,"slug":46},{"id":244,"slug":46,"title":245,"_count":250,"questionCount":51},{"questions":51},[252,254,258,260,264,266,270,274,278,280,283,287],{"slug":67,"name":68,"articles":253},161,{"slug":255,"name":256,"articles":257},"javascript","Javascript",130,{"slug":70,"name":71,"articles":259},92,{"slug":261,"name":262,"articles":263},"tutorial","Tutorial",63,{"slug":61,"name":65,"articles":265},49,{"slug":267,"name":268,"articles":269},"performance","Performance",27,{"slug":271,"name":272,"articles":273},"typescript","Typescript",23,{"slug":275,"name":276,"articles":277},"react","React",19,{"slug":73,"name":74,"articles":279},16,{"slug":281,"name":282,"articles":279},"browser","Browser",{"slug":284,"name":285,"articles":286},"node","Node",14,{"slug":288,"name":289,"articles":290},"html","Html",11,[],{"items":293,"meta":422},[294,306,325,345,364,384,404],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":295,"likeCount":19,"commentCount":19,"author":296,"vertical":297,"topic":298,"tags":299,"_count":304,"playground":305,"hasQuiz":17,"hasPlayground":17},32,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[300,301,302,303],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":307,"slug":308,"title":309,"subtitle":48,"excerpt":310,"coverUrl":311,"locale":13,"readingMinutes":312,"publishedAt":313,"viewCount":314,"likeCount":19,"commentCount":19,"author":315,"vertical":316,"topic":317,"tags":318,"_count":323,"playground":324,"hasQuiz":17,"hasPlayground":17},"01a1749f-27d5-773f-8c09-13beb3128da0","css-grid-lanes-native-masonry-layout","Stop Using a JS Masonry Library. CSS Grid Lanes Does It Natively.","Masonry layouts have meant a JS library or a CSS column-count hack that scrambles reading order ever since Pinterest made the look popular. CSS Grid Lanes finally does it natively — here's the real support story and how to ship it today.","\u002Fmedia\u002Fcovers\u002Fcss-grid-lanes-native-masonry-layout.png",6,"2026-10-30T05:36:15.418Z",149,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[319,320,321,322],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":261,"name":262,"color":48},{"assessments":76},{"slug":308},{"id":326,"slug":327,"title":328,"subtitle":48,"excerpt":329,"coverUrl":330,"locale":13,"readingMinutes":312,"publishedAt":331,"viewCount":332,"likeCount":19,"commentCount":19,"author":333,"vertical":334,"topic":335,"tags":336,"_count":343,"playground":344,"hasQuiz":17,"hasPlayground":17},"01a1749f-22b7-73c8-9c8c-4074d47d2832","font-loading-api-font-display-cls","font-display: swap Fixes Invisible Text. It Also Shifts Your Layout.","Setting font-display: swap kills the flash of invisible text, so most teams call the font-loading problem solved and move on. It doesn't — it just trades FOIT for a layout shift Lighthouse will happily dock you for. Here's the actual fix.","\u002Fmedia\u002Fcovers\u002Ffont-loading-api-font-display-cls.png","2026-10-29T06:10:25.373Z",190,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[337,338,341,342],{"slug":61,"name":65,"color":48},{"slug":339,"name":340,"color":48},"webperf","Webperf",{"slug":67,"name":68,"color":48},{"slug":267,"name":268,"color":48},{"assessments":76},{"slug":327},{"id":346,"slug":347,"title":348,"subtitle":48,"excerpt":349,"coverUrl":350,"locale":13,"readingMinutes":351,"publishedAt":352,"viewCount":353,"likeCount":19,"commentCount":19,"author":354,"vertical":355,"topic":356,"tags":357,"_count":362,"playground":363,"hasQuiz":17,"hasPlayground":17},"01a1623b-6184-751c-ba70-d14968adfe52","css-scroll-state-container-queries","Stop Writing JS to Detect a Stuck Header. `scroll-state()` Fixes It.","A sticky header that should only cast a shadow once it's actually stuck to the top usually means a hidden sentinel div and an IntersectionObserver. CSS now tracks that state itself — the `scroll-state()` container query reads it directly, no sentinel, no observer, no JS math.","\u002Fmedia\u002Fcovers\u002Fcss-scroll-state-container-queries.png",5,"2026-10-27T06:10:31.568Z",195,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[358,359,360,361],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":347},{"id":365,"slug":366,"title":367,"subtitle":48,"excerpt":368,"coverUrl":369,"locale":13,"readingMinutes":351,"publishedAt":370,"viewCount":371,"likeCount":19,"commentCount":19,"author":372,"vertical":373,"topic":374,"tags":375,"_count":382,"playground":383,"hasQuiz":17,"hasPlayground":17},"01a149cb-71f3-71ba-bb39-4f894fa2f1a8","css-individual-transform-properties","Stop Jamming Translate, Rotate, and Scale Into One `transform`","A CSS :hover scale and a JS drag handler both write to `transform` — and the moment JS sets it, the hover rule silently stops working, no error, no warning. The `translate`, `rotate`, and `scale` properties exist so this never happens.","\u002Fmedia\u002Fcovers\u002Fcss-individual-transform-properties.png","2026-10-22T06:10:24.373Z",252,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[376,377,378,379],{"slug":61,"name":65,"color":48},{"slug":255,"name":256,"color":48},{"slug":67,"name":68,"color":48},{"slug":380,"name":381,"color":48},"animation","Animation",{"assessments":76},{"slug":366},{"id":385,"slug":386,"title":387,"subtitle":48,"excerpt":388,"coverUrl":389,"locale":13,"readingMinutes":51,"publishedAt":390,"viewCount":391,"likeCount":19,"commentCount":19,"author":392,"vertical":393,"topic":394,"tags":395,"_count":402,"playground":403,"hasQuiz":17,"hasPlayground":17},"01a133e2-3e89-757c-ae01-38a8ad8aa448","forced-colors-high-contrast-mode","Stop Shipping Custom UI That Disappears in `forced-colors` 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> vanishes while a real \u003Cbutton> survives, and the fix.","\u002Fmedia\u002Fcovers\u002Fforced-colors-high-contrast-mode.png","2026-10-18T10:15:03.062Z",263,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[396,397,400,401],{"slug":61,"name":65,"color":48},{"slug":398,"name":399,"color":48},"accessibility","Accessibility",{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":386},{"id":405,"slug":406,"title":407,"subtitle":48,"excerpt":408,"coverUrl":409,"locale":13,"readingMinutes":312,"publishedAt":410,"viewCount":411,"likeCount":19,"commentCount":19,"author":412,"vertical":413,"topic":414,"tags":415,"_count":420,"playground":421,"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","2026-10-16T09:23:09.005Z",300,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[416,417,418,419],{"slug":61,"name":65,"color":48},{"slug":267,"name":268,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":406},{"page":76,"perPage":51,"total":265,"totalPages":51},"\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">:root\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#3b6fd6\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-var\">brand-hover\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#2f58ab\u003C\u002Fspan>;      \u003Cspan class=\"shj-cmnt\">\u002F* \"a bit darker\" — picked by eye, once *\u002F\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand-active\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#244289\u003C\u002Fspan>;     \u003Cspan class=\"shj-cmnt\">\u002F* \"darker still\" — same deal *\u002F\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand-border\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#6e92e1\u003C\u002Fspan>;     \u003Cspan class=\"shj-cmnt\">\u002F* \"a bit lighter\" *\u002F\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand-tint\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">rgba\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">59\u003C\u002Fspan>, \u003Cspan class=\"shj-num\">111\u003C\u002Fspan>, \u003Cspan class=\"shj-num\">214\u003C\u002Fspan>, \u003Cspan class=\"shj-num\">0.08\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F* \"faint background wash\" *\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">:root\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#3b6fd6\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-cmnt\">\u002F* \"same hue and chroma as --brand, just darker\" *\u002F\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand-hover\u003C\u002Fspan>:  \u003Cspan class=\"shj-func\">oklch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-func\">calc\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">l\u003C\u002Fspan> - \u003Cspan class=\"shj-num\">0.12\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">c\u003C\u002Fspan> \u003Cspan class=\"shj-num\">h\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-var\">brand-active\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">oklch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-func\">calc\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">l\u003C\u002Fspan> - \u003Cspan class=\"shj-num\">0.20\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">c\u003C\u002Fspan> \u003Cspan class=\"shj-num\">h\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-cmnt\">\u002F* \"same color, lighter, for a border\" *\u002F\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand-border\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">oklch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-func\">calc\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">l\u003C\u002Fspan> + \u003Cspan class=\"shj-num\">0.15\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">c\u003C\u002Fspan> \u003Cspan class=\"shj-num\">h\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-cmnt\">\u002F* \"same color, 8% opaque\" *\u002F\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">brand-tint\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">oklch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">l\u003C\u002Fspan> \u003Cspan class=\"shj-num\">c\u003C\u002Fspan> \u003Cspan class=\"shj-num\">h\u003C\u002Fspan> \u002F \u003Cspan class=\"shj-num\">8\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F* rgb()'s own channels: r, g, b (0–255), alpha *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">.badge\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">background\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">rgb\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">r\u003C\u002Fspan> \u003Cspan class=\"shj-num\">g\u003C\u002Fspan> \u003Cspan class=\"shj-num\">b\u003C\u002Fspan> \u002F \u003Cspan class=\"shj-num\">15\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-cmnt\">\u002F* hsl()'s own channels: h, s, l, alpha — s and l come back as plain\n   numbers (0-100), not percentages, so the math below uses 10, not 10% *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">.link\u003C\u002Fspan>\u003Cspan class=\"shj-var\">:hover\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">color\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">hsl\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">h\u003C\u002Fspan> \u003Cspan class=\"shj-num\">s\u003C\u002Fspan> \u003Cspan class=\"shj-func\">calc\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">l\u003C\u002Fspan> - \u003Cspan class=\"shj-num\">10\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-oneline\" data-lang=\"css\">--\u003Cspan class=\"shj-var\">brand-hover\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">oklch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">from\u003C\u002Fspan> \u003Cspan class=\"shj-func\">var\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>--\u003Cspan class=\"shj-num\">brand\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-func\">clamp\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>, \u003Cspan class=\"shj-func\">calc\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">l\u003C\u002Fspan> - \u003Cspan class=\"shj-num\">0.12\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>, \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">c\u003C\u002Fspan> \u003Cspan class=\"shj-num\">h\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":428},[]]