CSS variables, formally called CSS custom properties, are properties whose names begin with two hyphens, such as --brand-color, and whose values are read elsewhere with the var() function. They let a stylesheet store a value once and reuse it everywhere, and unlike preprocessor variables they are live in the browser: they follow the cascade, inherit through the DOM and can change at runtime.
In CSS and front-end development, custom properties are the foundation of design tokens, theming, dark mode and component configuration. A single change to a variable on the root element or on a component can update every value that depends on it, without rebuilding the stylesheet.
How CSS custom properties work
A custom property is declared like any other property, for example --space-m: 16px; inside a selector, and used with var(--space-m). Declarations on :root make a variable available to the entire document, while declarations on a component selector scope it to that subtree. Names are case-sensitive, so --Color and --color are different variables.
Custom properties take part in the normal cascade and specificity rules and are inherited by default. A child element therefore sees the nearest ancestor’s value unless it declares its own. The browser substitutes the value only when computing the property that uses it, which is why a variable can be redefined inside a media query or by a class and immediately affect every dependent declaration.
The var() function accepts a fallback value as its second argument, as in var(--accent, steelblue). The fallback is used when the variable is not defined. If a variable exists but produces an invalid value for the property, the declaration becomes invalid at computed-value time and falls back to the property’s inherited or initial value, not to an earlier declaration.
| Construct | Example | Effect |
|---|---|---|
| Declaration | –radius: 8px | Defines a custom property on an element |
| Global scope | :root with –brand: #0057b8 | Makes the value available document-wide |
| Usage | border-radius: var(–radius) | Substitutes the stored value |
| Fallback | var(–gap, 1rem) | Uses 1rem when –gap is undefined |
| Calculation | calc(var(–space) * 2) | Builds derived values from variables |
| Runtime update | style.setProperty in JavaScript | Changes the value without new CSS |
CSS variables compared with preprocessor variables
CSS variables are often compared with Sass variables, but the two operate at different times. Sass variables are resolved during compilation and disappear from the output, while custom properties remain in the delivered CSS and are resolved by the browser for each element.
| Aspect | CSS custom properties | Sass variables |
|---|---|---|
| Resolved | In the browser at runtime | At build time |
| Scope | DOM tree, through inheritance | Lexical blocks in the source file |
| Responds to media queries | Yes, can be redefined inside them | No, values are already fixed |
| Changeable by JavaScript | Yes | No |
| Usable in selectors and media query conditions | No | Yes |
| Logic and loops | Limited to calc and similar functions | Full functions, maps and loops |
In practice the two are complementary rather than competing. Many teams use Sass or a token build tool to generate a list of custom properties, then let the browser handle theming and responsive changes. Utility frameworks such as Tailwind CSS likewise expose their design tokens as custom properties.
Typed custom properties with the property rule
Ordinary custom properties are untyped token streams: the browser does not know whether --angle holds a degree, a color or arbitrary text. The @property at-rule, and its JavaScript equivalent, registers a custom property with a syntax, an inheritance behavior and an initial value.
Registration has two important benefits. First, the browser can animate and transition the variable, interpolating, for example, a registered angle inside a gradient, which is impossible with an unregistered property. Second, invalid values fall back to the registered initial value, making components more robust.
| Descriptor | Purpose | Example value |
|---|---|---|
| syntax | Defines the accepted value type | length, color, angle or percentage types |
| inherits | Controls inheritance to descendants | true or false |
| initial-value | Default when nothing valid is set | 0deg or transparent |
Practical patterns for CSS variables
Custom properties are most valuable when they encode design decisions rather than raw values. A common structure uses two layers: primitive tokens such as --blue-600 and semantic tokens such as --color-action that reference them. Components consume only the semantic layer, so a theme switch redefines the semantic values while components stay untouched.
- Theming and dark mode: redefine semantic color variables under a theme class or a prefers-color-scheme media query.
- Component APIs: expose variables such as –card-padding so that consumers adjust a component without overriding its internals.
- Responsive values: change spacing or type scale variables at a breakpoint or inside a container query.
- JavaScript bridges: write pointer positions or scroll progress into variables and let CSS handle the visual response.
| Need | Recommended location | Reason |
|---|---|---|
| Brand-wide design tokens | :root | Available to every element |
| Theme variations | Theme class or color-scheme media query | Overrides tokens in one place |
| Component settings | Component’s own selector | Keeps values scoped and predictable |
| One-off instance change | Inline style or modifier class | Adjusts a single element safely |
Pitfalls and limitations
Custom properties are powerful but have behaviors that surprise developers used to preprocessors or static values.
- No use in media query conditions: a breakpoint cannot be written as a variable inside a media query condition.
- Silent failures: a typo in a variable name produces no error, only an invalid or fallback value.
- Invalid at computed-value time: a wrong type does not revert to the previous declaration, which can unexpectedly reset a property.
- Unregistered variables do not animate smoothly; they switch discretely unless registered with a type.
- Too many layers of indirection make debugging harder; browser developer tools show computed values, but long chains still obscure intent.
A practical recommendation is to name variables by purpose, document the token layers, and provide fallbacks only where a variable may legitimately be absent.
Stripped to the essentials
CSS variables are named containers for style values such as colors, sizes and spacing. Instead of typing the same blue in two hundred places, a developer stores it once under a name and refers to that name everywhere.
When the stored value changes, every place that uses it updates at once. Because this happens live in the browser, a website can switch to a dark theme or bigger spacing instantly, without loading a different stylesheet.
Where it is applied
A software company maintains a design system used by six web applications. Colors were once hard-coded in hundreds of component styles, so adding a dark theme seemed to require editing every file. The front-end team introduces semantic custom properties such as --color-surface, --color-text and --color-action, defined on :root.
A dark theme then becomes a single block of overrides under a theme class and a prefers-color-scheme media query. The team also registers an angle variable with @property so that a loading indicator’s gradient rotates smoothly.
All six applications gain a consistent dark theme after updating only the token file, and later brand color changes are delivered by editing one value instead of searching the codebase.