CSS Custom Properties Beyond the :root

Manuel asked:

Is there a good reason why we’re defining global custom properties on :root/html and not on body?”

It’s a great question: Everybody just seems to define most of their global custom properties (aka CSS variables) on the :root selector without giving it a second thought – and so am I. But why :root?

The answer is that there is no real reason. It’s just a convention. Defining custom properties on the :root selector might make them look a bit more like global variables, because :root is the equivalent of the root element of your document. In HTML, this is the html element. In SVG, it is the svg element. But besides this potential gain of flexibility – in case you ever wanted to apply the same properties to different types of documents – there is no real technical benefit. You could just as well define all global custom properties on the body element and they would still work exactly the same way.

Sure, you would need to write your JavaScript in a slightly different way, if you wanted to work with your custom properties in JS. Instead of accessing them via the document root element like this:

getComputedStyle(document.documentElement).getPropertyValue("--color")

you would need to retrieve or set their values on the body element:

getComputedStyle(document.body).getPropertyValue("--color")

But this also is just a matter of preference.

Also, the fact that :root has a higher specificity than html might be interesting to know but also doesn’t really make a difference in practice.

The Limits of Inheritance

So yes, there seem to be no real differences between using :root and body. And therefore, using :root, as it is the outermost element, seems to make the most sense. However, Manuel pointed me to the fact that because of how the CSS inheritance model works, using :root does not always guarantee that a custom property is indeed inherited into all elements. It doesn’t currently work for the ::backdrop pseudo-element, for example, which is the element behind elements in full screen mode like <dialog>, as well as for the ::selection pseudo-element that lets you style the parts of a document that a user has selected. In both cases, trying to use a custom property that has been defined on :root will prove fruitless. The CSS Working Group is discussing ways to solve this. But it is definitely an issue many people might not be aware of.

Avoiding :root Growth

Regardless of those inconsistencies in inheritance and although we could also define them on <body>, using the :root selector to define our global custom properties has become a common practice. But what’s really interesting to me is that by using this convention, we also seem to have become a bit complacent in the way we use custom properties. Because defining most or all of your custom properties on :root can also become a problem. Kevin Powell has written about this on CSS-Tricks: if you just use this single place for all your custom properties, you might be able to manage all the “settings” of your CSS in one place, but you will also lose a lot of flexibility. When building components, for example, it is important to design and code them in such a way that you can easily change the implementation or add or remove them without affecting the global code around them or even breaking other parts of the system. Globally defined local properties would make that a lot harder. There is just no reason to define local custom properties in :root that actually belong to individual components.

As Lea Verou noted in her brilliant talk CSS Variable Secrets at CSS Day last year, we are also still not leveraging the full potential of custom properties when it comes to their depth, meaning how many properties inherit or include values from other properties. Or, to put it in another way, we don’t put custom properties inside each other very often. One reason might be that we are still mostly treating CSS custom properties like constants. We mostly use them as replacements for fixed values that we only have to define in one central place, which often happens to be, you guessed it, :root. Leaving this habit behind, however, and understanding that custom properties can do so much more is the key to unlocking their full potential.

To give you just one example derived from Lea’s talk, you can use custom properties locally that are basically like private variables but based on a global variable. Sounds complicated? Imagine a simple button component with a background color stored in a global custom property:

/* tokens.css */
:root {
	--color: hsl(160, 100%, 75%);
}

/* button.css */
button {
  background-color: var(--color);
}

This would work well in general and would also inherit the color value nicely into the button component. In a design system (or a modular website), this is exactly what you want. However, it is always a good idea to provide a fallback for custom properties, also because the component would then still work well even without the --color property being defined. With a local custom property that is based on the global property but already has the fallback baked in, you maintain the flexibility of being able to manipulate the color from the outside of the component. At the same time, you make the implementation much more robust:

/* tokens.css */
:root {
	--color: hsl(160, 100%, 75%);
}

/* button.css */
button {
  --_color: var(--color, black)
  background-color: var(--_color);
}

If we now use the button component within a system that provides a --color, the button has a background in the color we defined. If not, the component still works and uses the fallback color, black, instead. Inside of our component, we can use the local custom property (written with an underscore, again a convention) to define hover styles, borders, box-shadows, and more. And, we can still manipulate or set the --color variable from the outside of our component without breaking it. As Lea puts it: CSS variables effectively allow us to create a higher-level styling API.

This is just one powerful example which shows that combining global with local properties can lead to much more flexible and robust CSS. So while it might be still a good idea to define your global custom properties on :root as a best practice, also try to think beyond that. You can use CSS custom properties everywhere all at once.

~

29 Webmentions

Photo of @matthiasott
@matthiasott
@matthiasott I think one reason people that don’t do that is that the syntax is decidedly awkward and there is no tooling to see errors (as opposed to SCSS).
Photo of @matthiasott
@matthiasott
@matthiasott Everytime I see the awesome stuff people are doing with native CSS now, it makes me loathe CSS-in-JS tomfoolery even more. Hopefully someday I can convince my team to move away from it!
Photo of @matuzo
@matuzo
@matthiasott @matuzo Getting accessible style definitions, by not overload :root scope. Well done!
Photo of @matuzo
@matuzo
@matthiasott No, I didn't think that it makes sense to define it in :root cause of performance. https://lisilinhart.info/posts/css-variables-performance/ I mean it might depend on the count of calc() and overall project size. I did so for the theming like for colors https://github.com/redaktor/widgets-preview/blob/master/src/theme/material/_color.m.css or typo, e.g to generate the “solid baseline ...
Photo of @sl007
@sl007
@sl007 That’s interesting… the test that Lisi mentions is not online anymore but on the screenshot it looks like it used JavaScript to test the performance of different custom property manipulations. Would be interesting to see whether this is slower – which would be my guess – than the CSS object model. 🧐;

Likes

Reposts