CSS custom properties (--variable) are live, cascading values accessed via var() that can be scoped, inherited, overridden at any level, and manipulated with JavaScript at runtime — fundamentally different from preprocessor variables that are static and compiled away.
Custom properties participate in the CSS cascade and are inherited by descendants, enabling contextual theming by overriding values at any level in the DOM tree.
Custom properties exist in the live DOM and can be read with getComputedStyle() and written with setProperty() — enabling dynamic theming and user-driven customization.
The var() function accepts a fallback as a second argument, and fallbacks can be nested. If no value or fallback exists, the declaration becomes invalid at computed-value time.
Sass/Less variables are compiled away at build time with no cascade, inheritance, or runtime access. CSS custom properties are live, scoped, and manipulable — they serve different purposes.
CSS custom properties (often called CSS variables) are entities defined by authors that contain specific values reusable throughout a document. They participate in the cascade and inheritance, making them far more powerful than preprocessor variables.
Custom properties are declared with a double-hyphen prefix and accessed with the var() function:
:root {
--color-primary: #3b82f6;
--spacing-md: 1rem;
}
.button {
background: var(--color-primary);
padding: var(--spacing-md);
}The var() function accepts an optional fallback value: var(--color-primary, blue). Fallbacks can be nested: var(--color, var(--fallback, red)). If the custom property is invalid or not defined and no fallback exists, the property using it becomes invalid at computed-value time.
Custom properties cascade and inherit like any other CSS property. A property set on :root is available everywhere. A property set on .card is available to all .card descendants. This enables powerful scoping:
:root { --color-primary: blue; }
.dark-theme { --color-primary: lightblue; }
.card { background: var(--color-primary); }A .card inside .dark-theme automatically gets the overridden value without any additional selectors or classes.
Unlike preprocessor variables, custom properties exist in the live DOM and can be read and written with JavaScript:
// Read
getComputedStyle(element).getPropertyValue('--color-primary');
// Write
element.style.setProperty('--color-primary', '#ef4444');This enables dynamic theming, user preferences, responsive values driven by JavaScript, and animation targets — all impossible with Sass or Less variables.
| Aspect | CSS Custom Properties | Sass/Less Variables | |--------|----------------------|--------------------| | Runtime | Live in the browser | Compiled away at build time | | Cascade | Participate in cascade and inheritance | Static, no cascade awareness | | Scope | Scoped to any selector, inherited by children | Scoped to block or file | | JavaScript | Readable and writable at runtime | Not accessible at runtime | | Fallbacks | Built-in via var(--prop, fallback) | Requires conditional logic | | Media queries | Can be changed inside @media rules | Cannot change inside @media |
Preprocessor variables are still useful for compile-time constants (like breakpoint values used in @media queries), build-time calculations, and values that never change at runtime.
@property registration for typed animationsThe @property at-rule registers a custom property with a type, initial value, and inheritance behavior:
@property --rotation {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}Registered properties can be animated/transitioned, validated by the browser, and given default values — extending custom properties from simple text substitution to typed, animatable values.
Fun Fact
CSS custom properties were originally called 'CSS Variables' in the specification, but the working group renamed them because they behave nothing like variables in programming languages — they cascade, inherit, and can hold any valid CSS value including entire shorthand declarations.