I’ve seen this confuse more than a handful of people recently, including myself, so I’m making sure it’s written down.
Let’s chuck a couple of custom properties into CSS:
html {
--color-1: red;
--color-2: blue;
}
Let’s use them right away to make a background gradient:
html {
--color-1: red;
--color-2: blue;
--bg: linear-gradient(to right, var(--color-1), var(--color-2));
}
Now say there is a couple of divs sitting on the page:
<div></div>
<div class="variation"></div>
Lemme style them up:
div {
background: var(--bg);
}
That totally works! Hell yes!
Now lemme style that variation. I don’t want it to go from red to blue, I want it to go from green to blue. Easy cheesy, I’ll update red to green:
html {
--color-1: red;
--color-2: blue;
--bg: linear-gradient(to right, var(--color-1), var(--color-2));
}
div {
background: var(--bg);
}
.variation {
--color-1: green;
}
Nope! (Sirens blaring, horns honking, farm animals taking cover).
That doesn’t work, friends.
The problem, as best I understand it, is that --bg
was never declared on either of the divs. It can use --bg
, because it was declared higher up, but by the time it is being used there, the value of it is locked. Just because you change some other property that --bg
happens to use at the time it was declared, it doesn’t mean that property goes out searching for places it was used and updating everything that’s used it as a dependency.
Ugh, that explanation doesn’t feel quite right. But it’s the best I got.
The solution? Well, there are a few.
Solution 1: Scope the variable to where you’re using it.
You could do this:
html {
--color-1: red;
--color-2: blue;
}
div {
--bg: linear-gradient(to right, var(--color-1), var(--color-2));
background: var(--bg);
}
.variant {
--color-1: green;
}
Now that --bg
is declared on both divs, the change to the --color-1
dependency does work.
Solution 2: Comma-separate the selector where you set most of the variables.
Say you do the common thing where you set a bunch of variables at the :root
. Then you run into this problem. You can just add extra selectors to that main declaration to make sure you hit the right scope.
html,
div {
--color-1: red;
--color-2: blue;
--bg: linear-gradient(to right, var(--color-1), var(--color-2));
}
div {
background: var(--bg);
}
.variation {
--color-1: green;
}
In some other perhaps less-contrived example, it might look something like this:
:root,
.button,
.whatever-it-is-a-bandaid {
--padding-inline: 1rem;
--padding-block: 1rem;
--padding: var(--padding-block) var(--padding-inline);
}
.button {
padding: var(--padding);
}
.button.less-wide {
--padding-inline: 0.5rem;
}
Solution 3: Blanket Mode
Screw it — put the variables everywhere.
* {
--access: me;
--whereever: you;
--want: to;
--hogwild: var(--access) var(--whereever);
}
This is not a good plan. I overheard a chat recently in which a medium-sized site experienced a 500ms page rendering delay because every draw to the page needed to compute all the properties. It “works” but it’s one of the rare cases where you can cause legit performance problems with a selector.
Solution 4: Introduce a new “default” property and fallback
All credit here to Stephen Shaw who’s exploration on all this is one of the places I saw this confusion in the first place.
Let’s go back to our first demonstration of this problem:
html {
--color-1: red;
--color-2: blue;
--bg: linear-gradient(to right, var(--color-1), var(--color-2));
}
What we want to do is give ourselves two things:
- A way to override the entire background
- A way to overide a part of the gradient background
So we’re gonna do it this way:
html {
--color-1: red;
--color-2: blue;
}
div {
--bg-default: linear-gradient(to right, var(--color-1), var(--color-2));
background: var(--bg, var(--bg-default));
}
Notice that we haven’t declared --bg
at all. It’s just sitting there waiting for a value, and if it ever gets one, that’s the value that “wins.” But without one, it’ll fall back to our --bg-default
. Now…
- If I set
--color-1
or--color-2
, it replaces that part of the gradient as expected (so long as I do it on a selector that touches one of the divs). - Or, I can set
--bg
to reset the entire background to whatever I want.
Feels like a nice way to handle things.
Sometimes there are actual bugs with CSS custom properties. This isn’t one of them. Even though it sort of feels like a bug to me, apparently it’s not. Just one of those things you gotta know about.
The gold rule with CSS variables is to never do any evaluation at root level. It will make the variables useless because the root element (the html) is the uppermost element in the DOM tree and all the elements will simply inherit the “result” and not the “formula”.
A relevant SO question I always use: https://stackoverflow.com/q/52015737/8620333 (yes people always face this non-intuitive behavior and ask the same question on SO)
This said, there is a discussion about creating CSS functions to overcome this: https://github.com/w3c/css-houdini-drafts/issues/1007.
I really do not like how the html has to be marked up in a certain way for this css to behave as we wish. It’s strangely coupling concerns imo.
While this is a gotcha, the only proper solution is 1. 4 just adds a completely unnecessary layer of double overrides. What is the point of
--bg
? I either overridediv { background: ...}
or I overridediv {--color1: ...}
. Please don’t make the same mistake I’ve made for a long time :-)I prefer 4. I like how it gives options for usage.
I know it’s not pure CSS, but stuff like this is where SASS shines. You could declare just the colors as CSS variables, and then declare the gradient as a SASS mixin. When the SASS is compiled, you end up with the gradient being defined directly inside your div without losing the flexibility of the CSS variables to control it. Something like this:
Or you could simplify by including the CSS variables inside the mixin:
I define all custom properties in one place (:root), and don’t redefine properties locally. Helps avoid issues like this, and complexity in general.
`:root {
–color-1: red;
–color-2: blue;
–color-3: green;
}
div {
background: var(–bg-grad);
}
.variation {
background: var(–bg-grad-alt);
}`
linear-gradient()
in:root
.Hmmm,
calc()
does not appear to work in:root
.At least, for me :)
Can anyone confirm?
I wonder if other functions do not work in
:root
.Perhaps
:root
declarations are set before the browser renders anything and thus dimensions, of which I presume is the most common use ofcalc()
are not available?