CSS custom properties (variables) — real dynamic values
Unlike a Sass variable, a CSS custom property is a genuine, live browser feature — it participates in the cascade, can be read and changed by JavaScript, and can differ per element, none of which a preprocessor variable can do.
3 min read
Declaring and using one
:root {
--brand-color: #2563eb;
--spacing-unit: 8px;
}
.button {
background: var(--brand-color);
padding: calc(var(--spacing-unit) * 2); /* 16px, computed live */
}A custom property is declared with a -- prefix and read with var() — :root is the conventional place to declare page-wide values, since it's the highest-specificity element every other element inherits from. Combined with calc(), custom properties let derived values (padding as "2 units," a border as "1/4 of the spacing unit") stay expressed in terms of the single source of truth, rather than each place that needs a related value hardcoding its own number.
The critical difference from a Sass/Less variable: it's live, in the cascade, at runtime
.card { --card-padding: 16px; padding: var(--card-padding); }
.card.compact { --card-padding: 8px; } /* overrides the variable for THIS element and its descendants */A Sass variable is resolved once, at build time, into a plain static value baked into the compiled CSS — after that, it's just a number, indistinguishable from having been typed directly. A CSS custom property is resolved by the browser, live, following the normal cascade — which means --card-padding can have a different value on .card.compact than on a plain .card, inherited down to descendants exactly like any other CSS property, something a build-time preprocessor variable structurally cannot do since it has no concept of "the current element" at all.
Reading and writing custom properties from JavaScript
const root = document.documentElement;
const currentSpacing = getComputedStyle(root).getPropertyValue("--spacing-unit"); // "8px"
root.style.setProperty("--spacing-unit", "12px"); // every calc() using it updates LIVE, everywhereBecause custom properties are a genuine runtime feature of the browser's style engine, JavaScript can read and set them directly, and every rule anywhere in the page that references the changed property (via var()) recomputes immediately — no re-render logic, no re-applying classes, just the CSS engine reacting the same way it would to any other style change. This is the real mechanism behind a huge share of modern "theme switcher" implementations: toggling a handful of custom properties on :root (or a data-theme attribute) instead of swapping entire stylesheets.
A fallback value, for when the variable isn't defined
.button {
color: var(--button-text-color, white); /* uses white if --button-text-color isn't set anywhere */
}var()'s optional second argument is used whenever the referenced custom property is genuinely undefined (not just an empty string, which is a real, different case) — a small but useful safety net for a component meant to be reusable across contexts that might or might not define a given theme variable, without requiring every consumer to define every possible variable up front.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the critical difference between a CSS custom property and a Sass/Less variable?
2. What happens when JavaScript calls `root.style.setProperty('--spacing-unit', '12px')`?
3. What does the second argument in `var(--button-text-color, white)` do?