{"id":"l-css-1-4","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Use custom properties as a component’s public API, understand when var() substitution fails, and register typed properties with @property so they validate and animate.","html":"<p>Waypoint’s session cards come in four tracks (Design, Performance, Accessibility and Platform), and each track has its own color for the left border, the badge and the title hover state. The first version had a rule per track per element: twelve rules, and a thirteenth set every time marketing added a track.</p>\n<p>Custom properties turn that into one rule per track and a card that reads its color from a single dial.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A component with one dial</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">session-card.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:#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-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#64748b</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:#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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#64748b</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\">white</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:#0550AE;--shiki-dark:#79C0FF\">.track-design</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#db2777</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.track-perf</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#0891b2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span></code></pre></div>\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\">article</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"session-card track-design\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>Two things make this work. First, custom properties <strong>inherit</strong>: setting <code dir=\"ltr\">--track-color</code> on the card makes it visible to the badge inside. Second, <code dir=\"ltr\">var()</code> is resolved <strong>at the element that uses it</strong>, at computed-value time, so each card substitutes its own value. The second argument to <code dir=\"ltr\">var()</code> is the fallback, used only when the property isn’t set at all.</p>\n<p>Where you set a custom property matters. Global design tokens (brand colors, the spacing scale, font stacks) belong on <code dir=\"ltr\">:root</code>, so every element inherits them. Component tokens such as <code dir=\"ltr\">--track-color</code> belong on the component, so they stay local and one card can differ from its neighbour. Changing a property on <code dir=\"ltr\">:root</code> from JavaScript forces the browser to recompute styles for the whole tree that uses it, so for something that changes often, like a pointer position, set it on the smallest element that needs it.</p>\n<p>Think of <code dir=\"ltr\">--track-color</code> as the card’s public API. Anyone can theme a card from outside, in a CMS template or a one-off campaign page, without knowing its internals.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">When substitution goes wrong</h2>\n<p>Unregistered custom properties are just strings of tokens. The browser doesn’t know <code dir=\"ltr\">--track-color</code> is supposed to be a color, so <code dir=\"ltr\">--track-color: 12px</code> is a perfectly valid declaration. The problem appears later, when <code dir=\"ltr\">var()</code> drops <code dir=\"ltr\">12px</code> into <code dir=\"ltr\">border-inline-start</code>’s color.</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>Expecting CSS to fall back to the previous declaration</span></p>\n<p>With an ordinary typo like <code dir=\"ltr\">color: bleu</code>, the browser discards the declaration at parse time and the earlier one still applies. With <code dir=\"ltr\">var()</code>, the cascade has already picked the winning declaration before the value is known. When substitution produces nonsense, the property becomes <strong>invalid at computed-value time</strong> and gets its inherited or initial value, not your earlier rule. With a shorthand it’s worse: <code dir=\"ltr\">border-inline-start: 6px solid 12px</code> is invalid as a whole, so width, style and color all reset, and the initial style is <code dir=\"ltr\">none</code>.</p>\n</aside>\n<p>That’s how Waypoint shipped a card with no track border at all and a see-through badge: the CMS wrote <code dir=\"ltr\">12px</code> into the color field, and nothing complained.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Register the property with @property</h2>\n<p><code dir=\"ltr\">@property</code> tells the browser what a custom property is:</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:#CF222E;--shiki-dark:#FF7B72\">@property</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --track-color {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  syntax</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"&#x3C;color>\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inherits</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: true;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  initial-value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#64748b</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>Now three things change:</p>\n<ol>\n<li><strong>Validation.</strong> A value that isn’t a <code dir=\"ltr\">&lt;color&gt;</code> makes the property fall back safely: it behaves as unset, so an inherited property takes the parent’s value, which ends at the <code dir=\"ltr\">initial-value</code> if nobody sets it. The bad card gets a slate grey border instead of none.</li>\n<li><strong>Computed values are typed.</strong> <code dir=\"ltr\">getComputedStyle(card).getPropertyValue('--track-color')</code> returns <code dir=\"ltr\">rgb(219, 39, 119)</code>, not the raw text you wrote.</li>\n<li><strong>Interpolation.</strong> The browser knows how to blend between two colors, so the property can transition.</li>\n</ol>\n<p>The <code dir=\"ltr\">syntax</code> descriptor accepts types such as <code dir=\"ltr\">&lt;color&gt;</code>, <code dir=\"ltr\">&lt;length&gt;</code>, <code dir=\"ltr\">&lt;length-percentage&gt;</code>, <code dir=\"ltr\">&lt;number&gt;</code>, <code dir=\"ltr\">&lt;percentage&gt;</code>, <code dir=\"ltr\">&lt;angle&gt;</code>, <code dir=\"ltr\">&lt;time&gt;</code> and <code dir=\"ltr\">&lt;integer&gt;</code>, plus <code dir=\"ltr\">&quot;*&quot;</code> for anything. <code dir=\"ltr\">initial-value</code> is required unless the syntax is <code dir=\"ltr\">&quot;*&quot;</code>, and it must be computationally independent: <code dir=\"ltr\">10px</code> is fine, <code dir=\"ltr\">2em</code> isn’t.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Animating what used to jump</h2>\n<p>Here’s a hover effect that’s only possible with registration. The card’s top edge is a gradient whose angle is a custom property:</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:#CF222E;--shiki-dark:#FF7B72\">@property</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --sheen-angle {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  syntax</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"&#x3C;angle>\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inherits</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: false;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  initial-value</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\">deg</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:#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\">linear-gradient</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\">--sheen-angle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#f5f3ff</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">white</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 60</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:#0550AE;--shiki-dark:#79C0FF\">  transition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: --sheen-angle </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">400</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease</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:hover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --sheen-angle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">160</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">deg</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>Without <code dir=\"ltr\">@property</code>, the browser can’t interpolate a token string, so the gradient snaps from one angle to the other. With it, the angle animates smoothly. <code dir=\"ltr\">inherits: false</code> is right here: the angle belongs to this card’s background, and children shouldn’t receive it.</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>Support</span></p>\n<p><code dir=\"ltr\">@property</code> is Baseline 2024 (newly available): Chrome, Edge, Safari and Firefox all support it as of Firefox 128 in July 2024. In older browsers the property still works as an unregistered one, so treat registration as progressive enhancement. You can also register from JavaScript with <code dir=\"ltr\">CSS.registerProperty({ name, syntax, inherits, initialValue })</code>.</p>\n</aside>\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>Name tokens by role, not by value</span></p>\n<p><code dir=\"ltr\">--track-color</code> survives a rebrand; <code dir=\"ltr\">--pink-600</code> doesn’t. Name the custom properties a component reads after what they do, and map them to raw palette values in one place.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Your turn</h2>\n<p>The exercise reproduces the CMS bug: one card’s <code dir=\"ltr\">--track-color</code> is <code dir=\"ltr\">12px</code>. Register the property so bad values fall back to slate grey, and check that real track colors still come through. The next lesson keeps working on these cards with native nesting and <code dir=\"ltr\">:has()</code>, which lets a card react to what’s inside it.</p>\n","toc":[{"id":"sec-1","text":"A component with one dial","level":2},{"id":"sec-2","text":"When substitution goes wrong","level":2},{"id":"sec-3","text":"Register the property with @property","level":2},{"id":"sec-4","text":"Animating what used to jump","level":2},{"id":"sec-5","text":"Your turn","level":2}],"takeaways":["Custom properties inherit by default and are substituted where <code dir=\"ltr\">var()</code> is used, so setting one on a parent re-themes every descendant that reads it.","A <code dir=\"ltr\">var()</code> that produces an invalid value makes the property invalid at computed-value time; the browser uses the inherited or initial value, not your earlier declaration.","An unregistered custom property is an untyped string of tokens, so the browser can’t validate it or interpolate it in a transition.","<code dir=\"ltr\">@property</code> gives a custom property a syntax, an initial value and an inheritance flag, which makes bad values fall back safely and lets the property animate."],"further":[{"title":"Using CSS custom properties (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties"},{"title":"@property (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@property"},{"title":"@property: giving superpowers to CSS variables (web.dev)","url":"https://web.dev/articles/at-property"}],"quiz":[{"q":"<p>What padding does <code dir=\"ltr\">.card</code> get?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><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\">.card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --space</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">blue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  padding</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  padding</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\">--space</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","options":[{"text":"24px, because the browser falls back to the previous valid declaration.","why":"The fallback-to-earlier-declaration behaviour only applies to values that are invalid at parse time. <code dir=\"ltr\">var()</code> is checked later, after the cascade has already picked the last declaration."},{"text":"0, the initial value of padding.","why":"Correct. <code dir=\"ltr\">padding: var(--space)</code> wins the cascade, then turns out invalid at computed-value time, so padding falls back to its initial value."},{"text":"The browser ignores <code dir=\"ltr\">--space</code> and uses the <code dir=\"ltr\">var()</code> fallback, which is empty.","why":"There’s no fallback in <code dir=\"ltr\">var(--space)</code>, and fallbacks are only used when the property is missing (guaranteed-invalid), not when its value is the wrong type."}],"answer":1},{"q":"<p>You register <code dir=\"ltr\">@property --angle { syntax: '&lt;angle&gt;'; inherits: false; initial-value: 0deg; }</code>. What does this give you that an unregistered <code dir=\"ltr\">--angle</code> doesn’t?</p>","options":[{"text":"The ability to use <code dir=\"ltr\">--angle</code> inside <code dir=\"ltr\">var()</code>.","why":"Unregistered custom properties work in <code dir=\"ltr\">var()</code> too; registration isn’t required for substitution."},{"text":"Faster rendering, because typed properties skip the cascade.","why":"Registered properties go through the cascade like any other; registration changes parsing, validation and interpolation, not cascade cost."},{"text":"Smooth transitions and animations of <code dir=\"ltr\">--angle</code>, because the browser knows how to interpolate angles.","why":"Correct. An untyped property can only flip discretely; a typed one interpolates, so a gradient driven by <code dir=\"ltr\">--angle</code> can rotate smoothly."}],"answer":2},{"q":"<p>Where should a session card’s accent color be set so that a <code dir=\"ltr\">.track-design</code> card and everything inside it uses pink?</p>","options":[{"text":"<code dir=\"ltr\">.track-design { --accent: #db2777; }</code>, with the card’s rules reading <code dir=\"ltr\">var(--accent)</code>.","why":"Correct. Custom properties inherit, so the card and its title, badge and border all pick up the value."},{"text":"<code dir=\"ltr\">:root { --accent: #db2777; }</code> inside a <code dir=\"ltr\">.track-design</code> media query.","why":"Media queries test the viewport, not the element, and <code dir=\"ltr\">:root</code> would recolour every card on the page."},{"text":"On each child element separately, because custom properties don’t inherit.","why":"Unregistered custom properties always inherit, and registered ones inherit unless you set <code dir=\"ltr\">inherits: false</code>."}],"answer":0},{"q":"<p>A registered property has <code dir=\"ltr\">syntax: '&lt;color&gt;'; inherits: true; initial-value: gray</code>, and a card sets <code dir=\"ltr\">--accent: 12px</code>. Its parent doesn’t set <code dir=\"ltr\">--accent</code>. What value does the card end up with?</p>","options":[{"text":"12px, because custom properties accept any token.","why":"That’s true only for unregistered properties. Registration makes the browser check the value against <code dir=\"ltr\">&lt;color&gt;</code>."},{"text":"gray, inherited from the parent, which has the initial value.","why":"Correct. The invalid value makes the property behave as unset, which for an inherited property means inherit, and the parent holds the initial value."},{"text":"Nothing; the whole card rule is dropped.","why":"Only the invalid declaration is affected, and the browser recovers by treating it as unset."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"schedule\">\n  <article class=\"session-card track-design\" id=\"s1\">\n    <h2>Designing in oklch()</h2>\n    <span class=\"badge\">Design</span>\n  </article>\n  <article class=\"session-card track-perf\" id=\"s2\">\n    <h2>The Cost of a Frame</h2>\n    <span class=\"badge\">Performance</span>\n  </article>\n  <article class=\"session-card track-cms\" id=\"s3\">\n    <h2>Lightning Talks</h2>\n    <span class=\"badge\">Misc</span>\n  </article>\n</section>","css":"/* Register --track-color here */\n\n.session-card {\n  color: #1f2937;\n  margin-block-end: 12px;\n  padding: 12px 16px;\n  border-inline-start: 6px solid var(--track-color, #64748b);\n}\n.session-card h2 { margin: 0 0 8px; font-size: 1.1rem; }\n.badge {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n  color: white;\n  background: var(--track-color, #64748b);\n}\n\n.track-design { --track-color: #db2777; }\n.track-perf { --track-color: #0891b2; }\n/* Written by the CMS: someone typed a size into the color field */\n.track-cms { --track-color: 12px; }\n"},"solution":{"html":"<section class=\"schedule\">\n  <article class=\"session-card track-design\" id=\"s1\">\n    <h2>Designing in oklch()</h2>\n    <span class=\"badge\">Design</span>\n  </article>\n  <article class=\"session-card track-perf\" id=\"s2\">\n    <h2>The Cost of a Frame</h2>\n    <span class=\"badge\">Performance</span>\n  </article>\n  <article class=\"session-card track-cms\" id=\"s3\">\n    <h2>Lightning Talks</h2>\n    <span class=\"badge\">Misc</span>\n  </article>\n</section>","css":"@property --track-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: #64748b;\n}\n\n.session-card {\n  color: #1f2937;\n  margin-block-end: 12px;\n  padding: 12px 16px;\n  border-inline-start: 6px solid var(--track-color, #64748b);\n}\n.session-card h2 { margin: 0 0 8px; font-size: 1.1rem; }\n.badge {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n  color: white;\n  background: var(--track-color, #64748b);\n}\n\n.track-design { --track-color: #db2777; }\n.track-perf { --track-color: #0891b2; }\n/* Written by the CMS: someone typed a size into the color field */\n.track-cms { --track-color: 12px; }\n"},"checks":[{"id":"design-pink","test":"return style('#s1', 'border-left-color') === 'rgb(219, 39, 119)' && style('#s1 .badge', 'background-color') === 'rgb(219, 39, 119)';"},{"id":"perf-cyan","test":"return style('#s2', 'border-left-color') === 'rgb(8, 145, 178)';"},{"id":"bad-value-falls-back","test":"return style('#s3', 'border-left-color') === 'rgb(100, 116, 139)' && style('#s3 .badge', 'background-color') === 'rgb(100, 116, 139)';"},{"id":"registered-color","test":"return style('#s1', '--track-color').trim() === 'rgb(219, 39, 119)';"}],"title":"Register the track color","prompt":"<p>The CMS wrote <code dir=\"ltr\">12px</code> into the <strong>Lightning Talks</strong> card’s <code dir=\"ltr\">--track-color</code>, so its border and badge lose their color. Register <code dir=\"ltr\">--track-color</code> with <code dir=\"ltr\">@property</code> as a <code dir=\"ltr\">&lt;color&gt;</code> that <strong>inherits</strong> and has an initial value of <code dir=\"ltr\">#64748b</code> (slate grey). The Design (<code dir=\"ltr\">#db2777</code>) and Performance (<code dir=\"ltr\">#0891b2</code>) cards must keep their colors, and the broken card should fall back to slate grey.</p>\n","hints":["<code dir=\"ltr\">@property</code> takes three descriptors: <code dir=\"ltr\">syntax</code>, <code dir=\"ltr\">inherits</code> and <code dir=\"ltr\">initial-value</code>.","The syntax for a color is the string <code dir=\"ltr\">&quot;&lt;color&gt;&quot;</code>, with the quotes.","@property --track-color { syntax: “<color>”; inherits: true; initial-value: #64748b; }"],"explanation":"<p>Once <code dir=\"ltr\">--track-color</code> is registered as a <code dir=\"ltr\">&lt;color&gt;</code>, the browser checks every value against that type. <code dir=\"ltr\">12px</code> fails, so on the Lightning Talks card the property behaves as unset; because it inherits, it takes the parent’s value, and the parent only has the initial value, <code dir=\"ltr\">#64748b</code>. Valid track colors pass through and are now stored as real colors, which is why <code dir=\"ltr\">getComputedStyle</code> reports <code dir=\"ltr\">rgb(219, 39, 119)</code> instead of the text <code dir=\"ltr\">#db2777</code>. Without registration, <code dir=\"ltr\">12px</code> was substituted into the <code dir=\"ltr\">border-inline-start</code> shorthand, making the whole declaration invalid at computed-value time; every longhand reset to its initial value, including <code dir=\"ltr\">border-style: none</code>, so the border vanished.</p>\n","checkLabels":{"design-pink":"The Design card is pink","perf-cyan":"The Performance card is cyan","bad-value-falls-back":"The card with a bad value falls back to slate grey","registered-color":"–track-color is registered as a color"}},"words":763,"readMinutes":4}