CSS custom properties (also known as CSS variables) have revolutionized how we write stylesheets. They enable dynamic theming, cleaner code organization, and powerful runtime customization. But there's one behavior that trips up developers consistently--the "big gotcha" that makes custom properties feel like they don't work the way you'd expect.
Understanding this behavior is essential for anyone building modern, maintainable stylesheets. Once you know what to look for, you can avoid the pitfalls and use custom properties with confidence. This isn't a bug in CSS--it's a fundamental aspect of how the cascade and variable evaluation work that requires a shift in mental model from preprocessor variables.
Mastering custom properties is a key skill in modern web development, enabling developers to create more maintainable and flexible stylesheets.
Why This Matters
92%
of developers use CSS custom properties
78%
have encountered unexpected behavior
4
key solutions to master this gotcha
The Big Gotcha: Evaluation Timing
This is the issue that confuses developers more than any other. When you define a custom property that uses another custom property via var(), that compound value is evaluated at the point of declaration--not at the point of use. The cascade doesn't re-evaluate dependencies when a variable changes in a child scope.
The problem becomes clear in a common scenario: you define a gradient variable that depends on color variables at the :root level. Later, you try to override one of those colors in a child class expecting the gradient to update. It doesn't--because the gradient value was already computed and stored when --bg was declared.
This behavior trips up developers coming from SASS or LESS, where variables are static and replaced at compile time. CSS custom properties are live values that participate in the cascade, but that liveness has specific rules around when evaluation happens. As Chris Coyner documented on CSS-Tricks, understanding this distinction is crucial for writing predictable stylesheets.
If you're also working with SASS variables, understanding this difference is essential for migrating your codebase effectively.
1html {2 --color-1: red;3 --color-2: blue;4 --bg: linear-gradient(to right, var(--color-1), var(--color-2));5}6 7div {8 background: var(--bg);9}10 11.variation {12 --color-1: green; /* This doesn't update the gradient! */13}Solution 1: Scope Variables Where They're Used
The most reliable fix is to declare compound properties on the elements that use them. This ensures the variable is evaluated in the correct scope where all dependencies are available. When you move --bg to the div selector, the browser evaluates var(--color-1) and var(--color-2) within the context where they're actually used--meaning it sees the overridden values from .variation.
This approach aligns with a broader principle in modern CSS architecture: keep styles scoped as close to their usage as possible. Rather than centralizing all variables at :root, consider what each component needs and scope accordingly. The result is more predictable styling with fewer surprise interactions between unrelated rules.
For more on CSS architecture best practices, see our guide to writing maintainable CSS rules.
1html {2 --color-1: red;3 --color-2: blue;4}5 6div {7 --bg: linear-gradient(to right, var(--color-1), var(--color-2));8 background: var(--bg);9}10 11.variation {12 --color-1: green; /* Now this works! */13}Solution 2: Comma-Separate Selectors
When you want a compound property available globally but still overridable, you can declare it on multiple selectors simultaneously. By including div in the original declaration alongside html, the --bg variable is evaluated in both scopes. This means when .variation changes --color-1, the div elements within that scope correctly use the updated value.
This pattern is particularly useful when you have a shared design system where certain compound values need to be consistent across the site but also customizable in specific contexts. You're essentially creating multiple evaluation points for the same variable, each with its own cascade context. The browser handles this naturally--each scope maintains its own view of the variable dependencies.
This technique works especially well for responsive design patterns where components need to adapt across different contexts.
1html,2div {3 --color-1: red;4 --color-2: blue;5 --bg: linear-gradient(to right, var(--color-1), var(--color-2));6}7 8div {9 background: var(--bg);10}11 12.variation {13 --color-1: green; /* Works because --bg is also on div */14}Solution 3: Default Property and Fallback Pattern
A more flexible pattern introduces an "override" property that can completely replace the default, combined with a fallback in the var() function. By declaring --bg-default as the base gradient and using var(--bg, var(--bg-default)) in the actual styles, you create two distinct customization paths.
Components can either change the individual color variables that --bg-default depends on, or completely override --bg with a new value. This provides maximum flexibility--parent components can set defaults, and child components can either tweak the pieces or replace the whole thing. This pattern works especially well in component libraries where you need consistent defaults but also want to allow complete customization when needed.
For additional techniques on managing state-based styling with CSS, which also leverages custom properties effectively.
1html {2 --color-1: red;3 --color-2: blue;4}5 6div {7 --bg-default: linear-gradient(to right, var(--color-1), var(--color-2));8 background: var(--bg, var(--bg-default));9}10 11.variation {12 --bg: linear-gradient(to bottom, green, blue); /* Complete override */13}Performance Considerations
One "solution" sometimes suggested is declaring variables on the universal selector to ensure every element has access:
* {
--compound: var(--a) var(--b);
}
This is not recommended. Setting custom properties on every element can cause significant rendering delays because the browser must compute these properties for each element during every style recalculation. Real-world cases documented by developers have shown 500ms+ rendering delays from this pattern--genuine performance problems from what seems like innocent CSS.
Performance Best Practices
- Scope variables as narrowly as possible using specific selectors
- Use
:rootor element-specific selectors, not* - Avoid deeply nested variable dependencies that chain many
var()calls - Test performance on real devices with your target browsers
- Consider using CSS containment for complex components
This is one of the rare cases where CSS can cause measurable performance issues. The solution is simple: be intentional about where you declare variables and prefer specific selectors over universal ones. Proper CSS architecture also impacts SEO performance since site speed is a ranking factor.
Despite the gotchas, custom properties are incredibly valuable for these use cases
Dynamic Theming
Light/dark mode and brand color customization without duplicating stylesheets
Responsive Values
Values based on viewport or container queries for fluid typography and spacing
State-Based Styling
Clean hover, focus, and active state management without preprocessor limitations
Code Reuse
Reduce duplication across similar components while maintaining flexibility
JavaScript Integration
Update values at runtime for real-time customization and animations
Design Tokens
Centralize design decisions for consistency across large codebases
Quick Reference: The Golden Rules
- Variables are evaluated where declared, not where used -- Remember that compound values are locked at declaration time
- Scope compound variables to where they're used -- Move
--bgto the selector that needs it - Avoid custom properties in shorthand properties -- Use longhands or provide fallbacks
- Never scope to
*for performance reasons -- Use specific selectors instead - Use the fallback pattern for override flexibility --
var(--override, --default)provides both options
Conclusion
The "big gotcha" with custom properties isn't a bug--it's a fundamental aspect of how CSS evaluation works. Once you understand that custom properties are evaluated at their declaration point rather than dynamically, you can architect your stylesheets to leverage their power while avoiding surprises.
The key is intentional scoping: put your compound variables where they're meant to be used, and the cascade will work exactly as you'd expect. Custom properties remain one of the most powerful features in modern CSS. The gotcha isn't a reason to avoid them--it's knowledge that makes you better at using them.
Looking to level up your CSS architecture? Our web development team specializes in clean, maintainable stylesheets that leverage the full power of modern CSS features including custom properties, container queries, and cascade layers.
If you're building a modern web application, proper CSS architecture from the start prevents technical debt later. Our developers follow industry best practices to create scalable, maintainable frontends.
Frequently Asked Questions
Why don't CSS variables update when I change their dependencies?
CSS custom properties are evaluated at the point of declaration, not dynamically at use time. When you define `--compound: var(--color)`, the value of `--color` is captured immediately. Subsequent changes to `--color` don't re-evaluate `--compound`.
Should I avoid custom properties altogether because of this?
Absolutely not. Custom properties are one of the most powerful features in CSS. The gotcha is easily avoided once you understand it. The solution is simple: declare compound variables at the scope where they'll be used.
What's the difference between CSS custom properties and SASS variables?
SASS variables are compiled away--they're static values replaced at build time. CSS custom properties are live, inheritable values that can change at runtime and respond to the cascade. This power comes with the responsibility of understanding evaluation timing.
Can I use custom properties in media queries?
You can define custom properties inside media queries, but they won't update dynamically. For responsive values, use container queries with custom properties as values, or update properties via JavaScript. The properties themselves aren't responsive--only their values can be.
What's the best way to organize custom properties?
Use semantic naming at `:root` (e.g., `--color-primary`, `--spacing-md`) and component-scoped variables for component-specific values. Keep compound variables close to where they're used to avoid scope-related issues.