The Big Gotcha With Custom Properties

Why CSS variables don't always behave the way you expect--and how to fix them

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.

The Broken Pattern
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.

Solution 1: Component Scoping
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.

Solution 2: Selector Grouping
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.

Solution 3: Fallback Pattern
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 :root or 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.

When Custom Properties Shine

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

  1. Variables are evaluated where declared, not where used -- Remember that compound values are locked at declaration time
  2. Scope compound variables to where they're used -- Move --bg to the selector that needs it
  3. Avoid custom properties in shorthand properties -- Use longhands or provide fallbacks
  4. Never scope to * for performance reasons -- Use specific selectors instead
  5. 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.

Build Better Websites with Modern CSS

Our team specializes in clean, maintainable stylesheets that leverage the full power of CSS custom properties and modern web standards.