{"id":"l-css-3-4","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Ship a light and dark theme with color-scheme and light-dark(), add a user override that beats the system setting, and force a section dark without duplicating tokens.","html":"<p>Waypoint’s dark mode was built the classic way: a full set of color tokens in <code dir=\"ltr\">:root</code>, and a second full set inside <code dir=\"ltr\">@media (prefers-color-scheme: dark)</code>. It worked until three things happened. Someone added a token to the light set and forgot the dark one. Users asked for a toggle, because their OS is dark but they want the schedule light on a projector. And the design team wanted the hero to be dark in <em>both</em> modes.</p>\n<p>Each of these is a one-liner with <code dir=\"ltr\">color-scheme</code> and <code dir=\"ltr\">light-dark()</code>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">color-scheme: tell the browser what you support</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">theme.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:#0550AE;--shiki-dark:#79C0FF\">  color-scheme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">light</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> dark</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>This declaration says “this page supports both schemes; use the user’s preference”. The browser responds by rendering its own UI to match: form controls, checkboxes, scrollbars, the default page background and text color. Without it, a dark page still gets bright white <code dir=\"ltr\">&lt;select&gt;</code> menus and scrollbars, which is the most common tell of a hand-made dark mode.</p>\n<p>Add the matching meta tag so the browser knows before your CSS arrives, and the page doesn’t flash white on load:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">HTML</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:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">meta</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"color-scheme\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> content</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"light dark\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">light-dark(): one token, two values</h2>\n<p><code dir=\"ltr\">light-dark()</code> takes two colors and returns the first when the element’s used color scheme is light and the second when it’s dark:</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\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  color-scheme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">light</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> dark</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --surface</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: light-dark(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#ffffff</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#0f172a</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:    light-dark(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#1f2937</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#e5e7eb</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:    light-dark(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#f8fafc</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#1e293b</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:  light-dark(</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\">90</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.01</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 280</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\">35</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.02</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>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">body</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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--surface</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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--text</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</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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">--border</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>Each token is now defined once, with both values side by side. Adding a token without its dark value becomes visibly incomplete in review. <code dir=\"ltr\">light-dark()</code> for colors is Baseline 2024 and works in every current browser. Passing it two images (such as gradients or <code dir=\"ltr\">url()</code>s) only became Baseline in September 2026, and it never accepts lengths or other values, so for those keep a <code dir=\"ltr\">prefers-color-scheme</code> media query for now.</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>Forgetting color-scheme</span></p>\n<p><code dir=\"ltr\">light-dark()</code> doesn’t read the operating system directly. It reads the element’s <em>used color scheme</em>, which comes from the <code dir=\"ltr\">color-scheme</code> property. If nothing sets <code dir=\"ltr\">color-scheme: light dark</code> (or <code dir=\"ltr\">dark</code>), the scheme is light, and every <code dir=\"ltr\">light-dark()</code> returns its first argument, even on a dark OS.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">A toggle that beats the system setting</h2>\n<p>Because everything reads the color scheme, the toggle only has to change one property on the root:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><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\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">data-theme</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"light\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">color-scheme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">light</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">data-theme</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dark\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]  { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">color-scheme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">dark</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span></code></pre></div>\n<p>With no <code dir=\"ltr\">data-theme</code>, the root keeps <code dir=\"ltr\">light dark</code> and follows the OS. With <code dir=\"ltr\">data-theme=&quot;dark&quot;</code>, it’s forced dark, and every token follows. The JavaScript for a three-way control is tiny:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">theme-toggle.js</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:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> select</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#theme'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> saved</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'theme'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (saved) document.documentElement.dataset.theme </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> saved;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">select.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'change'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </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:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> select.value; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'system' | 'light' | 'dark'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (value </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'system'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.documentElement.dataset.theme;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">removeItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'theme'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">else</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    document.documentElement.dataset.theme </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> value;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'theme'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, value);</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>Run the restore part in a small inline script in the <code dir=\"ltr\">&lt;head&gt;</code> so the saved theme applies before the first paint.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 250\" role=\"img\" aria-labelledby=\"l-css-3-4-t1\"><defs><marker id=\"arrow-l-css-3-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-css-3-4-t1\">The root element has a color-scheme of light, dark, or both following the OS. Most elements inherit it. The hero sets color-scheme dark, so inside the hero every light-dark token returns its dark value, while the rest of the page returns light values.</title>\n  <rect class=\"d-box-primary\" x=\"20\" y=\"20\" width=\"260\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"36\" y=\"46\">:root color-scheme</text>\n  <text class=\"d-label-muted\" x=\"36\" y=\"68\">system / light / dark</text>\n  <path class=\"d-arrow\" d=\"M150 80 L150 120\" marker-end=\"url(#arrow-l-css-3-4-1)\"/>\n  <rect class=\"d-box\" x=\"20\" y=\"124\" width=\"260\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"36\" y=\"148\">.schedule inherits</text>\n  <text class=\"d-code\" x=\"36\" y=\"170\">light-dark(A, B) → A</text>\n  <rect class=\"d-box-accent\" x=\"360\" y=\"20\" width=\"300\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"376\" y=\"46\">.hero</text>\n  <text class=\"d-code\" x=\"376\" y=\"68\">color-scheme: dark</text>\n  <path class=\"d-arrow\" d=\"M510 80 L510 120\" marker-end=\"url(#arrow-l-css-3-4-1)\"/>\n  <rect class=\"d-box\" x=\"360\" y=\"124\" width=\"300\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"376\" y=\"148\">hero card inherits dark</text>\n  <text class=\"d-code\" x=\"376\" y=\"170\">light-dark(A, B) → B</text>\n  <path class=\"d-line d-dashed\" d=\"M280 50 L360 50\"/>\n  <text class=\"d-label-muted\" x=\"20\" y=\"222\">Same tokens everywhere; each element resolves them with its own scheme.</text>\n</svg><figcaption>The color scheme is resolved per element, then light-dark() picks a value</figcaption></figure>\n<h2 id=\"sec-4\" tabindex=\"-1\">Forcing a section dark</h2>\n<p><code dir=\"ltr\">color-scheme</code> is inherited, and <code dir=\"ltr\">light-dark()</code> resolves against each element’s own value. So the always-dark hero needs one declaration:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"5\"><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\">.hero</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  color-scheme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">dark</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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--surface</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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--text</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>Inside the hero, <code dir=\"ltr\">--surface</code>, <code dir=\"ltr\">--text</code> and <code dir=\"ltr\">--card</code> all resolve to their dark values, including the keynote session card nested inside it, while the rest of the page stays light. The same trick works the other way for a light “print preview” panel inside a dark app.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Designing the dark palette</h2>\n<p>Dark mode isn’t inverted light mode. A few rules that hold up on real projects:</p>\n<ul>\n<li><strong>Avoid pure black.</strong> A very dark blue-grey such as <code dir=\"ltr\">#0f172a</code> is easier on the eyes and leaves room for elevation.</li>\n<li><strong>Show elevation with lighter surfaces</strong>, not shadows: a card sits on <code dir=\"ltr\">#1e293b</code>, a popover on a step lighter. Shadows barely show on dark backgrounds.</li>\n<li><strong>Lower the chroma of accents.</strong> The vivid track colors from the last lesson vibrate on a dark background. <code dir=\"ltr\">oklch(from var(--track) 72% calc(c * 0.8) h)</code> raises lightness and tones down chroma for dark surfaces.</li>\n<li><strong>Recheck contrast in both schemes.</strong> A pairing that passes in light mode guarantees nothing in dark.</li>\n</ul>\n<p>You don’t have to switch your operating system to test any of this. Chrome’s DevTools can emulate <code dir=\"ltr\">prefers-color-scheme: dark</code> from the Rendering panel, and Firefox has a toggle in its Inspector. Test the three toggle states and the hero in each, and check the form controls: they are the first place a missing <code dir=\"ltr\">color-scheme</code> shows up.</p>\n<h2 id=\"sec-6\" tabindex=\"-1\">Your turn</h2>\n<p>The exercise uses the old pattern: tokens plus a <code dir=\"ltr\">prefers-color-scheme</code> override, and no toggle. Rewrite the tokens with <code dir=\"ltr\">light-dark()</code>, wire up <code dir=\"ltr\">data-theme</code> on the root, and make the hero dark in both modes. The checks set <code dir=\"ltr\">data-theme</code> and read the computed colors. Next, you’ll make sure people who navigate with a keyboard can see where they are, in both themes.</p>\n","toc":[{"id":"sec-1","text":"color-scheme: tell the browser what you support","level":2},{"id":"sec-2","text":"light-dark(): one token, two values","level":2},{"id":"sec-3","text":"A toggle that beats the system setting","level":2},{"id":"sec-4","text":"Forcing a section dark","level":2},{"id":"sec-5","text":"Designing the dark palette","level":2},{"id":"sec-6","text":"Your turn","level":2}],"takeaways":["<code dir=\"ltr\">color-scheme: light dark</code> tells the browser the page supports both schemes, so form controls, scrollbars and default colors follow the user’s setting.","<code dir=\"ltr\">light-dark(A, B)</code> returns A or B depending on the element’s used color scheme, so each token is defined once instead of in two places.","A three-way toggle (system, light, dark) only needs to set <code dir=\"ltr\">color-scheme</code> on the root; every <code dir=\"ltr\">light-dark()</code> value follows.","Because <code dir=\"ltr\">light-dark()</code> resolves per element, <code dir=\"ltr\">color-scheme: dark</code> on one section makes that section dark while the rest of the page stays light."],"further":[{"title":"light-dark() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/light-dark"},{"title":"color-scheme (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme"},{"title":"Improved dark mode default styling with color-scheme (web.dev)","url":"https://web.dev/articles/color-scheme"}],"quiz":[{"q":"<p>You write <code dir=\"ltr\">--surface: light-dark(white, #0f172a)</code> and use it on <code dir=\"ltr\">body</code>, but the page stays white in dark mode. What’s missing?</p>","options":[{"text":"A <code dir=\"ltr\">@media (prefers-color-scheme: dark)</code> block around the token.","why":"That’s the older technique; <code dir=\"ltr\">light-dark()</code> exists so you don’t need it. It reads the color scheme instead."},{"text":"<code dir=\"ltr\">color-scheme: light dark</code> on the root (or the element).","why":"Correct. Without it, the used color scheme is light, so <code dir=\"ltr\">light-dark()</code> always returns its first value."},{"text":"<code dir=\"ltr\">@property</code> registration for <code dir=\"ltr\">--surface</code>.","why":"Unregistered custom properties work fine here; <code dir=\"ltr\">light-dark()</code> is resolved where the token is used."},{"text":"Dark mode only works when the color is written in oklch().","why":"<code dir=\"ltr\">light-dark()</code> accepts any color format."}],"answer":1},{"q":"<p>The Waypoint hero should always look dark, even in light mode. Its colors come from <code dir=\"ltr\">light-dark()</code> tokens. What’s the smallest change?</p>","options":[{"text":"Duplicate every token with a <code dir=\"ltr\">--hero-</code> prefix holding the dark values.","why":"It works, but it doubles the token set and drifts the moment someone edits one copy."},{"text":"Add <code dir=\"ltr\">data-theme=&quot;dark&quot;</code> to the hero element.","why":"That attribute only matters if your CSS reads it there; the toggle in this lesson reads it on the root."},{"text":"Add <code dir=\"ltr\">color-scheme: dark</code> to <code dir=\"ltr\">.hero</code>.","why":"Correct. <code dir=\"ltr\">light-dark()</code> resolves against each element’s own used color scheme, which inherits, so the hero and its descendants pick the dark values."}],"answer":2},{"q":"<p>What’s the job of <code dir=\"ltr\">&lt;meta name=&quot;color-scheme&quot; content=&quot;light dark&quot;&gt;</code> in the HTML head?</p>","options":[{"text":"It lets the browser pick the right scheme for the page background before the CSS has loaded, avoiding a white flash in dark mode.","why":"Correct. The meta tag is read as soon as the HTML is parsed, so the initial canvas color is right from the first paint."},{"text":"It’s required for <code dir=\"ltr\">light-dark()</code> to work.","why":"The CSS <code dir=\"ltr\">color-scheme</code> property is enough for <code dir=\"ltr\">light-dark()</code>; the meta tag helps first paint."},{"text":"It replaces the <code dir=\"ltr\">prefers-color-scheme</code> media query in all stylesheets.","why":"Media queries keep working; the meta tag only declares which schemes the page supports."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"hero\">\n  <h1>Waypoint Conf 2026</h1>\n  <article class=\"session-card\" id=\"keynote\">Keynote · The Platform Is the Framework</article>\n</section>\n<main class=\"schedule\">\n  <article class=\"session-card\" id=\"regular\">Shipping View Transitions</article>\n</main>","css":":root {\n  --surface: #ffffff;\n  --text: #1f2937;\n  --card: #f8fafc;\n}\n@media (prefers-color-scheme: dark) {\n  :root {\n    --surface: #0f172a;\n    --text: #e5e7eb;\n    --card: #1e293b;\n  }\n}\n\nbody {\n  background: var(--surface);\n  color: var(--text);\n}\n.hero {\n  padding: 16px;\n  background: var(--surface);\n  color: var(--text);\n}\n.hero h1 { margin: 0 0 12px; font-size: 1.4rem; }\n.session-card {\n  margin-block: 8px;\n  padding: 12px;\n  border-radius: 12px;\n  background: var(--card);\n}\n"},"solution":{"html":"<section class=\"hero\">\n  <h1>Waypoint Conf 2026</h1>\n  <article class=\"session-card\" id=\"keynote\">Keynote · The Platform Is the Framework</article>\n</section>\n<main class=\"schedule\">\n  <article class=\"session-card\" id=\"regular\">Shipping View Transitions</article>\n</main>","css":":root {\n  color-scheme: light dark;\n  --surface: light-dark(#ffffff, #0f172a);\n  --text: light-dark(#1f2937, #e5e7eb);\n  --card: light-dark(#f8fafc, #1e293b);\n}\n:root[data-theme=\"light\"] { color-scheme: light; }\n:root[data-theme=\"dark\"] { color-scheme: dark; }\n\nbody {\n  background: var(--surface);\n  color: var(--text);\n}\n.hero {\n  color-scheme: dark;\n  padding: 16px;\n  background: var(--surface);\n  color: var(--text);\n}\n.hero h1 { margin: 0 0 12px; font-size: 1.4rem; }\n.session-card {\n  margin-block: 8px;\n  padding: 12px;\n  border-radius: 12px;\n  background: var(--card);\n}\n"},"checks":[{"id":"forced-dark","test":"const root = document.documentElement; const set = async (v) => { if (v) root.dataset.theme = v; else delete root.dataset.theme; await tick(16); }; await set(\"dark\"); let ok = false; try { ok = (style('body', 'background-color') === 'rgb(15, 23, 42)' && style('body', 'color') === 'rgb(229, 231, 235)' && style('#regular', 'background-color') === 'rgb(30, 41, 59)'); } finally { await set(null); } return ok;"},{"id":"forced-light","test":"const root = document.documentElement; const set = async (v) => { if (v) root.dataset.theme = v; else delete root.dataset.theme; await tick(16); }; await set(\"light\"); let ok = false; try { ok = (style('body', 'background-color') === 'rgb(255, 255, 255)' && style('body', 'color') === 'rgb(31, 41, 55)' && style('#regular', 'background-color') === 'rgb(248, 250, 252)'); } finally { await set(null); } return ok;"},{"id":"scheme-follows-toggle","test":"const root = document.documentElement; const set = async (v) => { if (v) root.dataset.theme = v; else delete root.dataset.theme; await tick(16); }; await set(\"dark\"); let ok = false; try { ok = (style(root, 'color-scheme') === 'dark'); } finally { await set(null); } return ok;"},{"id":"system-default","test":"const root = document.documentElement; const set = async (v) => { if (v) root.dataset.theme = v; else delete root.dataset.theme; await tick(16); }; await set(null); return style(root, 'color-scheme') === 'light dark';"},{"id":"hero-always-dark","test":"const root = document.documentElement; const set = async (v) => { if (v) root.dataset.theme = v; else delete root.dataset.theme; await tick(16); }; await set(\"light\"); let ok = false; try { ok = (style('.hero', 'color-scheme') === 'dark' && style('.hero', 'background-color') === 'rgb(15, 23, 42)' && style('#keynote', 'background-color') === 'rgb(30, 41, 59)' && style('#regular', 'background-color') === 'rgb(248, 250, 252)'); } finally { await set(null); } return ok;"}],"title":"One set of tokens, three themes","prompt":"<p>Rewrite the theme so that:</p>\n<ol>\n<li>The root declares support for <strong>both</strong> schemes, and <code dir=\"ltr\">--surface</code>, <code dir=\"ltr\">--text</code> and <code dir=\"ltr\">--card</code> are each defined <strong>once</strong> with <code dir=\"ltr\">light-dark()</code> (light values: <code dir=\"ltr\">#ffffff</code>, <code dir=\"ltr\">#1f2937</code>, <code dir=\"ltr\">#f8fafc</code>; dark: <code dir=\"ltr\">#0f172a</code>, <code dir=\"ltr\">#e5e7eb</code>, <code dir=\"ltr\">#1e293b</code>). Remove the media query.</li>\n<li><code dir=\"ltr\">data-theme=&quot;light&quot;</code> or <code dir=\"ltr\">data-theme=&quot;dark&quot;</code> on <code dir=\"ltr\">&lt;html&gt;</code> forces that scheme; with no attribute, the page follows the system.</li>\n<li>The <code dir=\"ltr\">.hero</code> is dark in every mode, using the same tokens (no hero-specific colors).</li>\n</ol>\n<p>The checks set <code dir=\"ltr\">data-theme</code> on <code dir=\"ltr\">&lt;html&gt;</code> and read computed colors.</p>\n","hints":["<code dir=\"ltr\">light-dark()</code> only works when <code dir=\"ltr\">color-scheme</code> allows dark. Start with <code dir=\"ltr\">color-scheme: light dark</code> on <code dir=\"ltr\">:root</code>.","The toggle rules only change <code dir=\"ltr\">color-scheme</code>: <code dir=\"ltr\">:root[data-theme=&quot;dark&quot;] { color-scheme: dark; }</code> and the same for light.","For the hero, add <code dir=\"ltr\">color-scheme: dark;</code> to <code dir=\"ltr\">.hero</code>. Its <code dir=\"ltr\">var(--surface)</code> and the nested card’s <code dir=\"ltr\">var(--card)</code> then resolve to the dark values."],"explanation":"<p><code dir=\"ltr\">light-dark()</code> picks a value from the used color scheme of the element where the token is used, and <code dir=\"ltr\">color-scheme</code> sets that scheme. <code dir=\"ltr\">color-scheme: light dark</code> on the root follows the OS; the <code dir=\"ltr\">data-theme</code> rules override it with a single property, so every token follows without a second token set. Because <code dir=\"ltr\">light-dark()</code> resolves per element and <code dir=\"ltr\">color-scheme</code> inherits, <code dir=\"ltr\">color-scheme: dark</code> on <code dir=\"ltr\">.hero</code> makes the hero and the keynote card inside it dark while the regular card stays light.</p>\n","checkLabels":{"forced-dark":"data-theme=“dark” makes the page dark","forced-light":"data-theme=“light” makes the page light","scheme-follows-toggle":"The root’s color-scheme follows the toggle","system-default":"With no data-theme, the root supports both schemes","hero-always-dark":"The hero and its card stay dark in light mode"}},"words":713,"readMinutes":3}