Back to CSS
CSS
easy
mid

What are CSS custom properties and how would you use them in production?

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.

5 min read·~8 min to think through

CSS custom properties — informally "CSS variables" — let you define reusable values directly in CSS that the browser evaluates at runtime.

Syntax

css
: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).
  • :root is 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:

css
.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:

js
// from JavaScript
el.style.setProperty("--color-primary", "#dc2626");
css
/* 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 propertiesSass variables
When resolvedRuntime, in the browserCompile time
Cascade / inheritYesNo (just text substitution)
Scoped to selectorsYesLexically scoped in source
Changeable by JS / media queriesYesNo
In DevToolsVisible, editableGone — 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 :root variables.
  • Design tokens — one source of truth for colors, spacing, typography.
  • Component APIs — expose --button-bg so 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 to inherited or initial — sometimes surprisingly.
  • They don't work in media query conditions (@media (min-width: var(--x)) — not allowed).
  • For animating them you may need @property to 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.

Related questions