Skip to content
Sahil Durgia/ full-stack
2 min readCSS & Tailwind

CSS Custom Properties (Variables): The Underused Superpower

Unlike a Sass variable, a CSS custom property is a live runtime value the browser can recompute — the entire mechanism behind a JS-free dark-mode toggle.

CSScustom propertiesdark mode

A Sass variable (`$primary-color`) is resolved once, at compile time — by the time your CSS ships, it's just a hardcoded value baked into every place it was used. A CSS custom property (`--primary-color`) is fundamentally different: it's a real, live value the browser holds onto and can recompute at runtime, which unlocks things a compile-time variable structurally cannot do.

The mechanism

:root { --accent: #b5abfc; }
.button { background: var(--accent); }

@media (prefers-color-scheme: dark) {
  :root { --accent: #201a45; }
}
/* Every element using var(--accent) updates automatically — no JS,
   no re-render, no re-selecting elements. The browser just re-resolves
   the variable and repaints. */

Because it's inherited and cascades like any other CSS property, redefining `--accent` at any point in the tree (a media query, a class toggle, a parent element) changes it for every descendant using `var(--accent)` beneath that point — live, automatically, with zero JavaScript.

Why this is the actual mechanism behind flash-free dark mode

This site's own theme system works exactly this way — light-mode values on `:root`, dark-mode overrides inside a `prefers-color-scheme: dark` media query and a `[data-theme="dark"]` attribute selector for an explicit manual toggle. Because the browser resolves custom properties before first paint, there's no flash of the wrong theme while JavaScript loads and figures out which theme to apply — the CSS cascade already has the answer by the time anything renders.

The other real use case: JS-controlled values without a re-render

document.documentElement.style.setProperty('--progress', `${percent}%`);
// CSS: .bar { width: var(--progress); transition: width 0.3s; }
// One JS call, no React re-render, no querySelector loop — the browser
// handles the animation entirely on the CSS side.

Setting a custom property directly via JavaScript, with CSS consuming it, is a genuinely useful pattern for a value that changes frequently (a scroll-linked progress bar, a drag position) — it avoids paying a JavaScript re-render cost for every single update, letting the CSS engine (and its own optimizations, like the ones the rendering-pipeline post in the fundamentals series covered) handle the visual update instead.

Keep reading
Next: why utility-first CSS won

Part 6 of the CSS and Tailwind series.