{"id":"l-css-3-3","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Define colors in oklch() so lightness is predictable, derive tints with color-mix(), and build hover and disabled states from one base color with relative color syntax.","html":"<p>Waypoint’s four track colors were picked one at a time in a design tool, as hex codes. The Design pink looked loud, the Accessibility green looked washed out, and the white badge text passed contrast on two tracks and failed on the other two. When the team needed a pale tint for each track’s badge background, someone generated eight more hex codes by eye. Then the hover states. Twenty colors, no system, and nobody could say why <code dir=\"ltr\">#0891b2</code> was right.</p>\n<p>Modern CSS color lets you define one base color per track and derive everything else with rules.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Why oklch()</h2>\n<p>Hex, <code dir=\"ltr\">rgb()</code> and <code dir=\"ltr\">hsl()</code> all describe colors in the sRGB space, organised around how screens emit light, not how eyes see it. HSL’s “lightness” is the worst offender: <code dir=\"ltr\">hsl(60 100% 50%)</code> (yellow) and <code dir=\"ltr\">hsl(240 100% 50%)</code> (blue) claim the same lightness, but the yellow is dazzling and the blue is dark. Any palette built by rotating the hue in HSL ends up with wildly different contrast.</p>\n<p><code dir=\"ltr\">oklch()</code> is built on the OKLab model, designed so that numbers track perception:</p>\n<ul>\n<li><strong>L</strong>, lightness, from 0 (black) to 1 or 100% (white), and equal L looks equally light across hues.</li>\n<li><strong>C</strong>, chroma, from 0 (grey) up to roughly 0.37 for the most vivid colors screens can show.</li>\n<li><strong>H</strong>, hue angle in degrees: around 30 is red, 140 green, 250 blue, 350 pink.</li>\n</ul>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">tokens.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --track-design</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">62</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.19</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 350</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --track-perf</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:   </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">62</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.19</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 220</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --track-a11y</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:   </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">62</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.19</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 150</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --track-platform</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">62</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.19</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 280</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Same lightness, same chroma, four hues. The tracks now carry equal visual weight, and if white text passes contrast on one, it will be very close on all four. You still verify contrast (perception models aren’t contrast formulas), but you start close instead of guessing.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Wide gamut for free</span></p>\n<p>OKLCH can describe colors outside sRGB, such as the vivid greens a P3 display can show. If a screen can’t display a color, the browser maps it to the nearest color it can. That’s why a high chroma like 0.25 can look a little different between a new phone and an old monitor. <code dir=\"ltr\">oklch()</code> has been Baseline since 2023 and widely available since late 2025.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Tints and shades with color-mix()</h2>\n<p><code dir=\"ltr\">color-mix()</code> blends two colors in a color space you choose:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.badge</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">color-mix</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">in</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">15</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">white</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">color-mix</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">in</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">70</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">black</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card.is-cancelled</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">color-mix</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">in</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">transparent</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 85</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The percentage belongs to the color it follows; the other color gets the rest. So <code dir=\"ltr\">var(--track) 15%, white</code> is mostly white with a hint of the track: a badge background. Mixing with <code dir=\"ltr\">transparent</code> produces a translucent version, without needing a separate alpha token.</p>\n<p>The color space changes the result. Mixing blue and yellow <code dir=\"ltr\">in srgb</code> passes through a muddy grey; <code dir=\"ltr\">in oklch</code> the hue travels around the wheel and stays vivid. For UI tints, <code dir=\"ltr\">oklch</code> or <code dir=\"ltr\">oklab</code> gives the most even steps. <code dir=\"ltr\">color-mix()</code> is Baseline 2023.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Darkening with mix-in black for hover states</span></p>\n<p><code dir=\"ltr\">color-mix(in srgb, var(--track), black 20%)</code> looks fine on pink, but sRGB isn’t perceptually even: the same 20% darkens some hues far more than others and dulls their chroma, so your four hover states no longer match. When you want “the same color, a bit darker” by a predictable amount, change only the lightness, which is what relative colors are for.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Relative colors: edit one channel</h2>\n<p>Relative color syntax unpacks a base color into its channels, lets you change any of them, and packs it back:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track-design</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border-inline-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:hover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.is-active</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    border-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(from </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">calc</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(l </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.15</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) c h);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.is-past</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    border-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(from </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) l </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.03</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h);   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* nearly grey, same hue */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> .badge-soft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(from </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) l c h / </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);  </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* same color, 20% alpha */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>After <code dir=\"ltr\">from var(--track)</code>, the keywords <code dir=\"ltr\">l</code>, <code dir=\"ltr\">c</code> and <code dir=\"ltr\">h</code> hold the base color’s channels as numbers, and you can use them in <code dir=\"ltr\">calc()</code>. Note that <code dir=\"ltr\">l</code> here is the 0 to 1 number, so subtracting 0.15 darkens by 15 points. Every derived state now follows the base color automatically: add a fifth track and its hover, past and soft variants exist the moment you define its base.</p>\n<p>Relative colors are Baseline 2024, in Chrome, Edge, Firefox and Safari. If you must support older browsers, don’t count on a static fallback declaration written first: these values contain <code dir=\"ltr\">var()</code>, so an older browser accepts them at parse time and then hits the invalid-at-computed-value-time trap from <a href=\"/lesson/css-mastery/l-css-1-4\">Custom Properties and @property</a>, discarding your fallback too. Wrap the derived states in <code dir=\"ltr\">@supports (color: oklch(from red l c h)) { … }</code> instead, so older browsers keep the base rule.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Read colors in DevTools as you go</span></p>\n<p>Chrome and Firefox show a swatch next to every color, and clicking it opens a picker that can display and convert OKLCH. When a derived color looks wrong, the computed value tells you exactly what <code dir=\"ltr\">l</code>, <code dir=\"ltr\">c</code> and <code dir=\"ltr\">h</code> came out as.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Your turn</h2>\n<p>The exercise card has hard-coded hex values for its badge tint and active border. Replace them with a <code dir=\"ltr\">color-mix()</code> tint and a relative-color active state, and add a Performance track that matches the Design track’s lightness and chroma. The checks read the computed OKLCH channels. Next, you’ll take this palette into dark mode with <code dir=\"ltr\">color-scheme</code> and <code dir=\"ltr\">light-dark()</code>.</p>\n","toc":[{"id":"sec-1","text":"Why oklch()","level":2},{"id":"sec-2","text":"Tints and shades with color-mix()","level":2},{"id":"sec-3","text":"Relative colors: edit one channel","level":2},{"id":"sec-4","text":"Your turn","level":2}],"takeaways":["<code dir=\"ltr\">oklch(L C H)</code> separates perceived lightness, chroma (colorfulness) and hue, so two colors with the same L look equally light, which HSL can’t promise.","A palette with fixed lightness and chroma and varying hue gives every category the same visual weight and similar contrast.","<code dir=\"ltr\">color-mix(in oklch, var(--track) 15%, white)</code> makes a tint from any base color, and the color space you mix in changes the result.","Relative color syntax, such as <code dir=\"ltr\">oklch(from var(--track) calc(l - 0.15) c h)</code>, derives a state color by editing one channel of the base color."],"further":[{"title":"oklch() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/oklch"},{"title":"color-mix() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix"},{"title":"Using relative colors (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Colors/Using_relative_colors"}],"quiz":[{"q":"<p><code dir=\"ltr\">hsl(60 100% 50%)</code> and <code dir=\"ltr\">hsl(240 100% 50%)</code> have the same lightness value. Why does white text read well on one and badly on the other?</p>","options":[{"text":"Because HSL lightness isn’t perceptual; pure yellow looks far lighter than pure blue at the same L.","why":"Correct. HSL is a geometric reshuffle of RGB. OKLCH’s L is designed to match perceived lightness, so equal L means similar contrast."},{"text":"Because the browser renders yellow with extra brightness on most screens.","why":"Screens render both as specified; the difference is in human perception, which HSL doesn’t model."},{"text":"Because 50% lightness is outside the sRGB gamut for blue.","why":"<code dir=\"ltr\">hsl()</code> colors are always inside sRGB; gamut isn’t the issue here."}],"answer":0},{"q":"<p>What does <code dir=\"ltr\">color-mix(in oklch, var(--track) 20%, white)</code> produce when <code dir=\"ltr\">--track</code> is a saturated pink?</p>","options":[{"text":"Pink at 20% opacity over whatever is behind it.","why":"Mixing with white produces an opaque color. Transparency would come from mixing with <code dir=\"ltr\">transparent</code>."},{"text":"A darker pink, because 20% is a small amount of white.","why":"The 20% applies to the pink; white gets the remaining 80%, so the result is much lighter."},{"text":"A pale pink tint, 20% pink and 80% white, interpolated in OKLCH.","why":"Correct. Percentages apply to the color they follow, and the other color gets the rest."},{"text":"An error, because custom properties can’t be used inside color-mix().","why":"<code dir=\"ltr\">var()</code> works inside <code dir=\"ltr\">color-mix()</code> like anywhere else; it’s substituted before the function is evaluated."}],"answer":2},{"q":"<p>Which declaration gives a hover state 0.1 darker than <code dir=\"ltr\">--track</code> but with the same chroma and hue, whatever <code dir=\"ltr\">--track</code> is?</p>","options":[{"text":"<code dir=\"ltr\">filter: brightness(0.9)</code>","why":"<code dir=\"ltr\">filter</code> darkens the whole element, including text and images, and works in RGB space, so hue can shift."},{"text":"<code dir=\"ltr\">background: oklch(from var(--track) calc(l - 0.1) c h)</code>","why":"Correct. Relative color syntax unpacks the base color into <code dir=\"ltr\">l</code>, <code dir=\"ltr\">c</code> and <code dir=\"ltr\">h</code>, lets you adjust one channel, and repacks it."},{"text":"<code dir=\"ltr\">background: oklch(calc(var(--track) - 0.1))</code>","why":"You can’t subtract from a whole color; without <code dir=\"ltr\">from</code>, <code dir=\"ltr\">oklch()</code> expects three separate channel values."}],"answer":1},{"q":"<p>Your track colors are written as <code dir=\"ltr\">oklch(62% 0.25 150)</code>, and on an older sRGB laptop the green looks slightly different from the designer’s P3 monitor. What happened?</p>","options":[{"text":"The browser rejected the color and used the fallback.","why":"The color is valid. Out-of-gamut colors aren’t rejected; they’re mapped into what the screen can show."},{"text":"OKLCH is not supported on that laptop.","why":"Support depends on the browser, not the screen, and every current browser supports <code dir=\"ltr\">oklch()</code>."},{"text":"The P3 monitor is miscalibrated.","why":"The difference is expected: the chroma is beyond what sRGB screens can display."},{"text":"The color is outside the sRGB gamut, so the browser mapped it to the closest color the screen can display.","why":"Correct. <code dir=\"ltr\">oklch()</code> can describe colors more vivid than sRGB. Wide-gamut screens show them; others get the nearest displayable color."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<article class=\"session-card track-design is-active\" id=\"design\">\n  <h3>Designing in oklch()</h3>\n  <span class=\"badge\">Design</span>\n</article>\n<article class=\"session-card track-perf\" id=\"perf\">\n  <h3>The Cost of a Frame</h3>\n  <span class=\"badge\">Performance</span>\n</article>","css":".track-design { --track: oklch(62% 0.19 350); }\n\n.session-card {\n  margin-block-end: 12px;\n  padding: 12px 16px;\n  border-radius: 12px;\n  border-inline-start: 6px solid var(--track, #94a3b8);\n}\n.session-card h3 { margin: 0 0 8px; }\n.badge {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n}\n\n/* Hand-picked values: replace them with rules derived from --track */\n.badge { background: #fce7f3; }\n.session-card.is-active { border-inline-start-color: #9d174d; }\n\n/* Add the Performance track here */\n"},"solution":{"html":"<article class=\"session-card track-design is-active\" id=\"design\">\n  <h3>Designing in oklch()</h3>\n  <span class=\"badge\">Design</span>\n</article>\n<article class=\"session-card track-perf\" id=\"perf\">\n  <h3>The Cost of a Frame</h3>\n  <span class=\"badge\">Performance</span>\n</article>","css":".track-design { --track: oklch(62% 0.19 350); }\n\n.session-card {\n  margin-block-end: 12px;\n  padding: 12px 16px;\n  border-radius: 12px;\n  border-inline-start: 6px solid var(--track, #94a3b8);\n}\n.session-card h3 { margin: 0 0 8px; }\n.badge {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n}\n\n.track-perf { --track: oklch(62% 0.19 220); }\n\n.badge { background: color-mix(in oklch, var(--track) 15%, white); }\n.session-card.is-active {\n  border-inline-start-color: oklch(from var(--track) calc(l - 0.15) c h);\n}\n"},"checks":[{"id":"badge-tint","test":"const ok = (s) => { const m = /oklch\\(([\\d.]+) ([\\d.]+) ([\\d.]+)/.exec(s); return m ? m.slice(1).map(Number) : null; }; const near = (a, b, t) => Math.abs(a - b) <= t; const c = ok(style('#design .badge', 'background-color')); return !!c && near(c[0], 0.943, 0.01) && near(c[1], 0.0285, 0.006) && near(c[2], 350, 2);"},{"id":"active-darker","test":"const ok = (s) => { const m = /oklch\\(([\\d.]+) ([\\d.]+) ([\\d.]+)/.exec(s); return m ? m.slice(1).map(Number) : null; }; const near = (a, b, t) => Math.abs(a - b) <= t; const c = ok(style('#design', 'border-left-color')); return !!c && near(c[0], 0.47, 0.01) && near(c[1], 0.19, 0.01) && near(c[2], 350, 1);"},{"id":"perf-track","test":"const ok = (s) => { const m = /oklch\\(([\\d.]+) ([\\d.]+) ([\\d.]+)/.exec(s); return m ? m.slice(1).map(Number) : null; }; const near = (a, b, t) => Math.abs(a - b) <= t; const c = ok(style('#perf', 'border-left-color')); return !!c && near(c[0], 0.62, 0.01) && near(c[1], 0.19, 0.01) && near(c[2], 220, 1);"},{"id":"perf-badge-follows","test":"const ok = (s) => { const m = /oklch\\(([\\d.]+) ([\\d.]+) ([\\d.]+)/.exec(s); return m ? m.slice(1).map(Number) : null; }; const near = (a, b, t) => Math.abs(a - b) <= t; const c = ok(style('#perf .badge', 'background-color')); return !!c && near(c[0], 0.943, 0.01) && near(c[2], 220, 2);"},{"id":"derived-for-new-tracks","test":"const ok = (s) => { const m = /oklch\\(([\\d.]+) ([\\d.]+) ([\\d.]+)/.exec(s); return m ? m.slice(1).map(Number) : null; }; const near = (a, b, t) => Math.abs(a - b) <= t; const card = $('#perf'); card.classList.add('is-active'); await tick(16); const c = ok(style(card, 'border-left-color')); card.classList.remove('is-active'); await tick(16); return !!c && near(c[0], 0.47, 0.01) && near(c[2], 220, 1);"}],"title":"Derive the track colors","prompt":"<p>The Design track’s base color is <code dir=\"ltr\">--track: oklch(62% 0.19 350)</code>, but its badge tint and active border are hand-picked hex codes. Replace them with rules that work for <strong>any</strong> track:</p>\n<ol>\n<li>Badge background: mix <strong>15%</strong> of <code dir=\"ltr\">--track</code> with <strong>white</strong>, in <strong>oklch</strong>.</li>\n<li>Active card (<code dir=\"ltr\">.is-active</code>) border: the track color with lightness <strong>0.15 lower</strong>, same chroma and hue, using relative color syntax.</li>\n<li>Add a <code dir=\"ltr\">.track-perf</code> class whose <code dir=\"ltr\">--track</code> has the same lightness and chroma as Design, with hue <strong>220</strong>.</li>\n</ol>\n<p>The checks read the computed OKLCH values, and also activate the Performance card to make sure your rules aren’t specific to one track.</p>\n","hints":["<code dir=\"ltr\">color-mix(in oklch, A 15%, B)</code> gives 15% of A and 85% of B.","Relative syntax starts with <code dir=\"ltr\">from</code>: <code dir=\"ltr\">oklch(from var(--track) … c h)</code>. The <code dir=\"ltr\">l</code> channel is a number from 0 to 1.","<code dir=\"ltr\">.badge { background: color-mix(in oklch, var(--track) 15%, white); }</code>, <code dir=\"ltr\">.is-active { border-inline-start-color: oklch(from var(--track) calc(l - 0.15) c h); }</code>, <code dir=\"ltr\">.track-perf { --track: oklch(62% 0.19 220); }</code>."],"explanation":"<p>Both state colors now read <code dir=\"ltr\">--track</code> at the element where they’re used, so every track gets its own tint and active border from one base value. <code dir=\"ltr\">color-mix(in oklch, var(--track) 15%, white)</code> interpolates lightness and chroma towards white while keeping the hue, giving a pale tint (L ≈ 0.94). The relative color form unpacks <code dir=\"ltr\">--track</code> into <code dir=\"ltr\">l</code>, <code dir=\"ltr\">c</code> and <code dir=\"ltr\">h</code>, subtracts 0.15 from the lightness and repacks it, so the active border is the same pink, only darker. Because the Performance track shares L and C with Design, its colors carry the same visual weight.</p>\n","checkLabels":{"badge-tint":"The Design badge is a 15% tint of the track color","active-darker":"The active border is 0.15 darker than the track","perf-track":"The Performance track has the same L and C with hue 220","perf-badge-follows":"The Performance badge tint follows its own track","derived-for-new-tracks":"An active Performance card gets its own darker border"}},"words":794,"readMinutes":4}