Learning on Web Dev Open is free for all.

Take It Apart > Boxes, and the engines that arrange themCustom properties as plumbing
Phase 01Boxes, and the engines that arrange them58 of 434

Custom properties as plumbing

Not variables. Values that inherit, substitute late, and have no type unless you give them one, which explains every surprise they produce.

Build28 minAI implements

A Sass variable is compiled away before the browser sees it. A custom property is a real value in the cascade, it inherits, it can be changed at runtime, it can differ per subtree, and JavaScript can read and write it. Those are different tools with the same shape, and confusing them is where the trouble starts.

Work the instrument below in order, because the three panels are increasingly uncomfortable and the third is the one worth the chapter.

The first panel is the easy half: tokens at :root, components reading them, one change re-theming everything. Then tick the subtree box and watch one declaration on one wrapper re-theme only what is inside it. That is inheritance doing exactly what it always does, and it is what makes per-section theming a one-liner, and what makes a token that is "wrong in one corner of the app" so hard to track down. The value is not global. It is wherever you last set it.

The second panel is the trap. Custom properties are substituted at computed-value time, which is after the cascade has already picked a winner. So a var() that fails does not lose gracefully to the previous declaration the way an ordinary typo does, it makes the whole declaration invalid at computed-value time, and the property falls to its inherited-or-initial value. Typo a colour token and your button is not the old colour, it is transparent. Try all four states in the panel; the last one, where a fallback is present and still does not save you, is the one that ends arguments.

That last state is worth stating plainly: var(--x, fallback) uses the fallback when --x is not set. Not when it is set to something wrong. It is a presence check, not a validity check, and a token holding a nonsense value sails straight past it.

The third panel is @property, which is the fix for all of that. Register a custom property with a syntax and the browser knows its type: a wrong value is rejected at parse time instead of poisoning a declaration later, and, the part people actually want, the value can be interpolated, so a registered <angle> or <color> can be animated where an unregistered one snaps.

Custom properties, including the parts that bite

1 · one place, many components
Cardbadge
Cardbadge
Cardbadge

Three components, one set of tokens, no component-specific CSS. This is the half every tutorial shows, and the half that never causes a bug. Tick the box below to start the half that does.

2 · what happens when a var() is wrong
The CSS
.button {
  background: var(--brand);
}
What renders: really, not a mock

The button is the brand colour. --brand is set and valid, so the substitution succeeds and the declaration behaves like any other.

3 · registering a property gives it a type
/* no @property rule */
:root { --sweep: 0deg; }

Unregistered, --sweep is just a token sequence. The browser cannot interpolate between "0deg" and "360deg" because as far as it knows those are strings, so any animation of it snaps. It also cannot reject a nonsense value, which is how panel 2 happens.

The useful mental model: a custom property is a value that inherits, substituted late, with no type unless you give it one. Every surprise in this component follows from one of those three.

Panel two is the one to sit with. A typo does not fall back to the old value, it falls out of the declaration entirely.

Now build. Convert your own project’s stylesheet to tokens: colour, spacing, radius, type scale. Add a dark theme by redefining the tokens under [data-theme="dark"] and nothing else, if you find yourself writing a second rule for a component, your tokens are not granular enough. Then scope a token on one section to prove subtree theming works. Then deliberately typo one and watch what happens, so you have seen it once in your own code.

Done when

  • A token set covering colour, spacing, radius and type scale
  • A working theme switch that changes only token values, no component rules
  • One token deliberately scoped to a subtree, with a comment explaining why it works
  • One @property registration with a syntax, used for something that animates
  • A note in the repo recording what happened when you typoed a var() name

Nobody marks this for you. It goes into your phase checkpoint, where a person does.

Build this in your own editor

This one runs on your machine rather than in the browser workbench. Work to the outcomes below, then come back and mark it complete.

Worth reading

You should now be able to

  • Build a theme with custom properties and switch it at runtime
  • Scope a token to a subtree and know why that works
  • Explain what happens when a var() is invalid, and why it is not what you expect
  • Use @property to give a token a type
Ask the community

Loading…