Skip to the content.

Custom properties are runtime variables, and that changes theming

Custom properties are runtime variables, and that changes theming

There are two kinds of “CSS variable” and the difference is when they exist. A Sass variable ($brand) is a build-time value: the preprocessor substitutes it and it is gone before the browser ever sees the CSS. A CSS custom property (--brand) is a runtime value: it lives in the cascade, it inherits down the tree, and — the crucial part — you can change it while the page is running and every rule that reads it updates instantly. That single property, being alive at runtime, is why modern theming, dark mode, and per-component overrides are built on custom properties and not on preprocessor variables.

Sass variables vanish at build; custom properties live in the running page Top: a Sass variable is substituted at build time and does not exist at runtime. Bottom: a custom property exists at runtime, inherits down the tree, and can be reassigned to retheme live. Sass $var build gone at runtime --var :root --brand inherits every consumer reassign --brand → all update live
Sass values are compiled away; custom properties persist in the page, inherit through the tree, and re-flow every consumer when you change one.

Define once, read everywhere, change at runtime

Declare custom properties on :root so they inherit to the whole document, then read them with var(). Because they are live, reassigning one on any element updates every rule that reads it under that element:

:root {
  --brand: #fe854c;
  --text: #1a1a1a;
  --bg: #ffffff;
}
.button { background: var(--brand); }
.page    { color: var(--text); background: var(--bg); }

Nothing here is compiled away — --brand is sitting in the running page, and the button’s colour is a live reference to it.

Theming is a one-selector override

Because custom properties cascade and inherit, a theme is just a different set of values scoped to a selector. Dark mode does not touch a single component rule — it redefines the tokens, and everything downstream re-resolves:

[data-theme="dark"] {
  --text: #e6e6e6;
  --bg: #161b22;
  --brand: #ff9a63;
}
/* no component CSS changes — .button and .page just read the new values */

Flip the attribute in one line of JS and the whole page rethemes, with no per-component logic:

document.documentElement.dataset.theme = "dark";   // every var() re-resolves

Read from JS, and scope per component

The runtime nature cuts both ways: JavaScript can read and write custom properties too, which is how you bridge dynamic values into CSS (a drag position, a computed accent) without inline-styling every rule. And because they inherit, you can override a token for one subtree — a card that wants a denser spacing scale sets --space: 4px on itself and its children pick it up, without new class names. Two caveats keep it honest: custom properties are not known to the preprocessor, so you cannot use them in Sass math at build time; and they resolve at use time, so a typo’d var(--brnad) silently falls back rather than erroring. Used well, they turn theming from a rebuild into a runtime toggle — which is exactly what the theme-toggle exercise builds: one attribute flip, a whole palette redefined, zero component edits.