What are CSS custom properties (variables)
CSS custom properties (`--name: value`) are real CSS variables — they cascade, inherit, can be scoped to any selector, read with `var(--name, fallback)`, and changed at runtime via JS or media queries. Unlike Sass variables (compile-time, static), they're live in the browser, which makes them ideal for theming, dark mode, and component APIs.
CSS custom properties — informally "CSS variables" — let you define reusable values directly in CSS that the browser evaluates at runtime.
Syntax
:root {
--color-primary: #2563eb;
--space-md: 16px;
}
.button {
background: var(--color-primary);
padding: var(--space-md);
color: var(--text-color, white); /* fallback if --text-color is unset */
}- Declared with the
--prefix. - Read with
var(--name, fallback). :rootis the common global scope, but they can be declared on any selector.
They cascade and inherit — that's the key feature
Custom properties follow the cascade and inherit down the DOM. This means you can scope and override them:
.card { --padding: 16px; }
.card.compact { --padding: 8px; } /* override in a subtree */
.card { padding: var(--padding); }They're live — change them at runtime
Unlike preprocessor variables, custom properties exist in the browser and can change:
// from JavaScript
el.style.setProperty("--color-primary", "#dc2626");/* from a media query — instant theming, no JS */
@media (prefers-color-scheme: dark) {
:root { --bg: #111; --text: #eee; }
}Changing one variable cascades to every var() that uses it.
CSS custom properties vs. Sass/Less variables
| CSS custom properties | Sass variables | |
|---|---|---|
| When resolved | Runtime, in the browser | Compile time |
| Cascade / inherit | Yes | No (just text substitution) |
| Scoped to selectors | Yes | Lexically scoped in source |
| Changeable by JS / media queries | Yes | No |
| In DevTools | Visible, editable | Gone — already compiled away |
They're not competitors — Sass is great for build-time logic (loops, mixins, math); custom properties are great for runtime, themeable values.
Primary use cases
- Theming & dark mode — flip a few
:rootvariables. - Design tokens — one source of truth for colors, spacing, typography.
- Component APIs — expose
--button-bgso consumers can customize without overriding internals. - Reducing repetition and keeping related values in sync.
Gotchas
- They're case-sensitive (
--Color≠--color). - An invalid
var()value falls back toinheritedorinitial— sometimes surprisingly. - They don't work in media query conditions (
@media (min-width: var(--x))— not allowed). - For animating them you may need
@propertyto register a type.
Senior framing
The senior answer centers on "runtime + cascade" — that's what makes them categorically different from Sass variables and what unlocks themeable design systems and JS-driven dynamic styling. Mentioning @property for typed/animatable custom properties, and that they can power a whole dark-mode implementation with zero JS, shows depth.
Follow-up questions
- •How do CSS custom properties differ from Sass variables?
- •How would you implement dark mode using custom properties?
- •What does the @property at-rule add?
- •Why can't you use var() inside a media query condition?
Common mistakes
- •Treating them as identical to Sass variables (they're runtime, not compile-time).
- •Expecting var() to work in media query conditions.
- •Forgetting they're case-sensitive.
- •Not providing fallbacks for var() where the property might be unset.
Edge cases
- •Invalid custom property values trigger 'invalid at computed value' fallback behavior.
- •Animating custom properties needs @property registration for interpolation.
- •Custom properties on :root vs scoped — inheritance determines what wins.
Real-world examples
- •Design-system tokens, dark/light theming, user-customizable accent colors, component theming APIs.