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
- Declare with a double-dash name (
--space-4: 1rem), and use withvar(--space-4). var(--x, fallback)provides a default when the variable is undefined.- Custom properties inherit and follow the cascade. Define globals on
:rootand override in any scope. - Theming: redefine a set of variables under
[data-theme="dark"]or@media (prefers-color-scheme: dark). @propertyregisters a typed variable with an initial value and inheritance, and makes it animatable.- Read and write them from JavaScript with
getComputedStyle(el).getPropertyValue('--x')andel.style.setProperty('--x', value).
Quick Example
A token-based theme with light and dark modes, plus a component API:
Core Concepts
Declaring and Using
- Names start with
--and are case-sensitive (--Brand≠--brand). - Values can be any valid token sequence: colors, lengths, numbers, lists, even partial values.
var(--name, fallback): the fallback applies when the property is undefined, or has the guaranteed-invalid initial value. Fallbacks can nest:var(--a, var(--b, 1rem)).- Combine them with
calc():margin: calc(var(--space-4) * 2),width: calc(100% - var(--sidebar-width)).
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:
- Primitive tokens: raw palette and scales (
--violet-600,--space-4). - Semantic tokens: purpose-based aliases (
--color-bg,--color-danger,--radius-control) that themes redefine. - 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
- CSS — The styling language overview
- Design Tokens — Structuring shared design values
- Dark Mode Theming — Light and dark themes in practice
- CSS Cascade & Specificity — How variables cascade and inherit
- CSS Container Queries — Style queries on custom properties
- Design Systems — Tokens and component APIs