Everyone calls them CSS variables. The spec calls them custom properties. This isn't pedantry — the distinction matters, and understanding it will change how you use them.
The thing that looks like a variable
:root {
--color-accent: #5eaaa8;
}
a {
color: var(--color-accent);
}
If you've seen this pattern, you probably thought "okay, it's a variable." You set a value once, reference it elsewhere, change it in one place and it updates everywhere. That's what variables do.
And yes, custom properties do that. But they also do something variables don't.
They cascade
This is the part people miss. Custom properties participate in the cascade. They inherit. They can be overridden per element, per class, per media query, per anything CSS can target.
:root {
--bg: #1a1a2e;
--text: #e8e6e3;
}
.card {
--bg: #252542;
background: var(--bg);
color: var(--text);
}
The .card has its own --bg. Everything inside .card that references --bg gets the card's value, not the root's. The --text property? Still inherited from :root because .card didn't override it.
This is inheritance, not variable scoping. It follows the DOM tree, not your file structure.
Why this matters in practice
A variable in Sass or JavaScript is resolved once, at compile time or assignment. A CSS custom property is resolved at render time, per element, based on where that element sits in the document.
This means you can do things like:
.theme-light {
--bg: #ffffff;
--text: #1a1a1a;
}
.theme-dark {
--bg: #1a1a2e;
--text: #e8e6e3;
}
Add the class to <html> and every element on the page that references --bg or --text updates. No JavaScript loop. No re-rendering. The browser's cascade does all the work.
Or you can scope them to components:
.btn {
--btn-bg: var(--accent);
--btn-text: white;
background: var(--btn-bg);
color: var(--btn-text);
}
.btn.danger {
--btn-bg: #e57373;
}
The .danger modifier only changes one property. Everything that depends on --btn-bg updates automatically. You never wrote a single background: override.
The fallback trick
var() takes an optional second argument — a fallback value:
color: var(--link-color, #5eaaa8);
If --link-color isn't defined in the current scope, it uses #5eaaa8. This is useful for optional customization — you can build components that work with sensible defaults but allow overrides.
What Sass variables can't do
Sass variables are resolved at compile time. They don't know about the DOM. They can't respond to media queries at runtime, can't be scoped to elements, can't be read or changed by JavaScript.
Custom properties can do all of that:
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a2e;
--text: #e8e6e3;
}
}
No JavaScript. No class toggling. The browser handles it.
And from JavaScript, you can read and write them:
// Read
getComputedStyle(element).getPropertyValue('--bg');
// Write
element.style.setProperty('--bg', '#ff0000');
This creates a clean interface between CSS and JavaScript. CSS owns the design tokens; JavaScript can modify them when needed without knowing anything about which elements use them.
When to use what
Use custom properties when:
- The value needs to change at runtime (themes, responsive adjustments, user preferences)
- You want component-level customization through the cascade
- JavaScript needs to interact with your styles
- You want fallback values for optional overrides
Use Sass/preprocessor variables when:
- The value is truly constant and never changes
- You need math or string operations at compile time (though CSS
calc()handles most math now) - You're building a design system's internal tooling, not its output
Most modern projects should use both. Sass variables for the build process, custom properties for the runtime.
The real mental shift
Stop thinking of --custom-property as "a variable I defined." Start thinking of it as "a property I invented." It cascades, inherits, and can be overridden — just like color or font-size. The var() function is how you read it.
Once that clicks, you start seeing possibilities everywhere. Every hardcoded value in your CSS becomes a question: "Should this be customizable? Should it inherit? Should it respond to context?"
Usually the answer is no. But when it's yes, custom properties are the cleanest solution CSS has ever had.
Comments
Loading comments...