CSS Custom Properties

Custom properties, commonly called CSS variables, let you define values once (--brand: #6d28d9) and reuse them with var(--brand). Unlike preprocessor variables in Sass or Less, which are replaced at build time, custom properties are live: they cascade, inherit through the DOM, can be changed per element or media query, and can be updated from JavaScript at runtime, with every usage updating instantly.

That makes them the foundation of modern theming (light and dark mode, brand themes, user preferences), component APIs (a button that exposes --button-bg for customization), and design token systems that connect design tools to code.

TL;DR

Quick Example

A token-based theme with light and dark modes, plus a component API:

Core Concepts

Declaring and Using

Inheritance and Scope

Custom properties inherit like color does. Define globals on :root, then override for a subtree:

This scoping is what makes contextual theming (a dark hero section, a compact table, per-brand areas) so simple. It also means a variable set on a component root is visible to all its descendants.

Computed-Value Time and Invalid Values

var() is substituted at computed-value time. If the substituted value is invalid for the property (--size: red; width: var(--size)), the declaration becomes invalid at computed-value time: the property falls back to its inherited or initial value, not to earlier declarations in the cascade. This is a common source of confusion when debugging.

Registered Properties With @property

By default, custom properties are untyped strings, so the browser can't interpolate them in transitions. @property gives them a type:

Registration enables animating gradients and other values, type checking (invalid values fall back to initial-value), and non-inheriting variables, which also improves performance for frequently changed values. It's supported in all modern browsers.

JavaScript Integration

Updating one variable is often cheaper and cleaner than toggling many inline styles.

Theming and Token Architecture

A common layered approach:

  1. Primitive tokens: raw palette and scales (--violet-600, --space-4).
  2. Semantic tokens: purpose-based aliases (--color-bg, --color-danger, --radius-control) that themes redefine.
  3. Component tokens: optional per-component hooks (--button-bg) defaulting to semantic tokens.

Themes (dark mode, high contrast, brands) override only semantic tokens. Components reference semantic or component tokens, never primitives, so a theme change needs no component edits. Tools like Style Dictionary generate these variables from design-tool tokens. See dark mode theming and design tokens.

Best Practices

Use Semantic Names in Components

--color-surface survives rebrands and theme switches; --light-gray doesn't. Name by role, not appearance.

Provide Fallbacks for Component APIs

var(--button-bg, var(--color-accent)) lets consumers customize a component while keeping sensible defaults, without extra classes.

Register Frequently Animated Variables

Use @property with inherits: false for variables updated per frame (pointer effects, animations), so style recalculation stays contained and values interpolate smoothly.

Keep Preprocessor Variables for Build-Time Constants

Sass variables still make sense for values used in selectors, media query breakpoints, or loops, since custom properties can't be used in media query conditions. Use custom properties for anything that should vary at runtime or by context.

Common Mistakes

Using Variables in Media Queries

Use preprocessor variables, or custom media queries (@custom-media) via PostCSS, for breakpoints. Container queries can use style queries for variable-driven changes.

Forgetting Units in calc()

Expecting Invalid Values to Fall Back to Earlier Rules

When a variable resolves to an invalid value for a property, the browser uses the inherited or initial value, not the previous declaration in your stylesheet. Validate token values, or register them with @property to get a typed initial value.

FAQ

What's the difference between CSS variables and Sass variables?

Sass variables are compiled away at build time into static values. CSS custom properties exist in the browser: they cascade, inherit, can differ per element or media query, and can be changed with JavaScript at runtime. Many projects use both, Sass for build-time logic and custom properties for theming.

Can I animate CSS custom properties?

Unregistered custom properties switch discretely, with no smooth interpolation. Registering them with @property and a syntax (such as <length>, <color>, <angle>, or <number>) makes them animatable with transitions and keyframes.

How do I implement dark mode with CSS variables?

Define semantic color variables on :root, and override them in a dark-theme scope ([data-theme="dark"] and/or @media (prefers-color-scheme: dark)). Components use only the semantic variables, so switching themes is just switching which values are active. Also set color-scheme: light dark so native controls match.

Do custom properties affect performance?

Generally not noticeably. Changing a variable on :root triggers style recalculation for everything that inherits it, which is usually cheap. For high-frequency updates, set variables on the smallest possible element, and register them with inherits: false.

Related Topics

References