CSS Custom Properties have been a hot topic for a while now, with tons of great articles about them, from great primers on how they work to creative tutorials to do some real magic with them. If you’ve read more than one or two articles on the topic, then I’m sure you’ve noticed that they start by setting up the custom properties on the :root
about 99% of the time.
While putting custom properties on the :root
is great for things that you need to be available throughout your site, there are times when it makes more sense to scope your custom properties locally.
In this article, we’ll be exploring:
- Why we put custom properties on the
:root
to begin with. - Why global scoping isn’t right for everything.
- How to overcome class clashing with locally scoped custom properties
What’s the deal with custom properties and :root?
Before we jump into looking at the global scope, I think it’s worth looking at why everyone sets custom properties in the :root
to begin with.
I’ve been declaring custom properties on the :root
without even a second thought. Pretty much everyone does it without even a mention of why — including the official specification.
When the subject of :root
is actually breached, it mentions how :root
is the same as html
, but with higher specificity, and that’s about it.
But does that higher specificity really matter?
Not really. All it does is select html
with a higher specificity, the same way a class selector has higher specificity than an element selector when selecting a div
.
:root {
--color: red;
}
html {
--color: blue;
}
.example {
background: var(--color);
/* Will be red because of :root's higher specificity */
}
The main reason that :root
is suggested is because CSS isn’t only used to style HTML documents. It is also used for XML and SVG files.
In the case of XML and SVG files, :root
isn’t selecting the html
element, but rather their root (such as the svg
tag in an SVG file).
Because of this, the best practice for a globally-scoped custom property is the :root
. But if you’re making a website, you can throw it on an html
selector and not notice a difference.
That said, with everyone using :root
, it has quickly become a “standard.” It also helps separate variables to be used later on from selectors which are actively styling the document.
Why global scope isn’t right for everything
With CSS pre-processors, like Sass and Less, most of us keep variables tucked away in a partial dedicated to them. That works great, so why should we consider locally scoping variables all of a sudden?
One reason is that some people might find themselves doing something like this.
:root {
--clr-light: #ededed;
--clr-dark: #333;
--clr-accent: #EFF;
--ff-heading: 'Roboto', sans-serif;
--ff-body: 'Merriweather', serif;
--fw-heading: 700;
--fw-body: 300;
--fs-h1: 5rem;
--fs-h2: 3.25rem;
--fs-h3: 2.75rem;
--fs-h4: 1.75rem;
--fs-body: 1.125rem;
--line-height: 1.55;
--font-color: var(--clr-light);
--navbar-bg-color: var(--clr-dark);
--navbar-logo-color: var(--clr-accent);
--navbar-border: thin var(--clr-accent) solid;
--navbar-font-size: .8rem;
--header-color: var(--clr-accent);
--header-shadow: 2px 3px 4px rgba(200,200,0,.25);
--pullquote-border: 5px solid var(--clr-light);
--link-fg: var(--clr-dark);
--link-bg: var(--clr-light);
--link-fg-hover: var(--clr-dark);
--link-bg-hover: var(--clr-accent);
--transition: 250ms ease-out;
--shadow: 2px 5px 20px rgba(0, 0, 0, .2);
--gradient: linear-gradient(60deg, red, green, blue, yellow);
--button-small: .75rem;
--button-default: 1rem;
--button-large: 1.5rem;
}
Sure, this gives us one place where we can manage styling with custom properties. But, why do we need to define my --header-color
or --header-shadow
in my :root
? These aren’t global properties, I’m clearly using them in my header and no where else.
If it’s not a global property, why define it globally? That’s where local scoping comes into play.
Locally scoped properties in action
Let’s say we have a list to style, but our site is using an icon system — let’s say Font Awesome for simplicity’s sake. We don’t want to use the disc
for our ul
bullets — we want a custom icon!
If I want to switch out the bullets of an unordered list for Font Awesome icons, we can do something like this:
ul {
list-style: none;
}
li::before {
content: "\f14a"; /* checkbox */
font-family: "Font Awesome Free 5";
font-weight: 900;
float: left;
margin-left: -1.5em;
}
While that’s super easy to do, one of the problems is that the icon becomes abstract. Unless we use Font Awesome a lot, we aren’t going to know that f14a
means, let alone be able to identify it as a checkbox icon. It’s semantically meaningless.
We can help clarify things with a custom property here.
ul {
--checkbox-icon: "\f14a";
list-style: none;
}
This becomes a lot more practical once we start having a few different icons in play. Let’s up the complexity and say we have three different lists:
<ul class="icon-list checkbox-list"> ... </ul>
<ul class="icon-list star-list"> ... </ul>
<ul class="icon-list bolt-list"> ... </ul>
Then, in our CSS, we can create the custom properties for our different icons:
.icon-list {
--checkbox: "\f14a";
--star: "\f005";
--bolt: "\f0e7";
list-style: none;
}
The real power of having locally scoped custom properties comes when we want to actually apply the icons.
We can set content: var(--icon)
on our list items:
.icon-list li::before {
content: var(--icon);
font-family: "Font Awesome Free 5";
font-weight: 900;
float: left;
margin-left: -1.5em;
}
Then we can define that icon for each one of our lists with more meaningful naming:
.checkbox-list {
--icon: var(--checkbox);
}
.star-list {
--icon: var(--star);
}
.bolt-list {
--icon: var(--bolt);
}
We can step this up a notch by adding colors to the mix:
.icon-list li::before {
content: var(--icon);
color: var(--icon-color);
/* Other styles */
}
Moving icons to the global scope
If we’re working with an icon system, like Font Awesome, then I’m going to assume that we’d be using them for more than just replacing the bullets in unordered lists. As long as we’re using them in more than one place it makes sense to move the icons to the :root
as we want them to be available globally.
Having icons in the :root
doesn’t mean we can’t still take advantage of locally scoped custom properties, though!
:root {
--checkbox: "\f14a";
--star: "\f005";
--bolt: "\f0e7";
--clr-success: rgb(64, 209, 91);
--clr-error: rgb(219, 138, 52);
--clr-warning: rgb(206, 41, 26);
}
.icon-list li::before {
content: var(--icon);
color: var(--icon-color);
/* Other styles */
}
.checkbox-list {
--icon: var(--checkbox);
--icon-color: var(--clr-success);
}
.star-list {
--icon: var(--star);
--icon-color: var(--clr-warning);
}
.bolt-list {
--icon: var(--bolt);
--icon-color: var(--clr-error);
}
Adding fallbacks
We could either put in a default icon by setting it as the fallback (e.g. var(--icon, "/f1cb")
), or, since we’re using the content
property, we could even put in an error message var(--icon, "no icon set")
.
See the Pen
Custom list icons with CSS Custom Properties by Kevin (@kevinpowell)
on CodePen.
By locally scoping the --icon
and the --icon-color
variables, we’ve greatly increased the readability of our code. If someone new were to come into the project, it will be a whole lot easier for them to know how it works.
This isn’t limited to Font Awesome, of course. Locally scoping custom properties also works great for an SVG icon system:
:root {
--checkbox: url(../assets/img/checkbox.svg);
--star: url(../assets/img/star.svg);
--baby: url(../assets/img/baby.svg);
}
.icon-list {
list-style-image: var(--icon);
}
.checkbox-list { --icon: checkbox; }
.star-list { --icon: star; }
.baby-list { --icon: baby; }
Using locally scoped properties for more modular code
While the example we just looked at works well to increase the readability of our code — which is awesome — we can do a lot more with locally scoped properties.
Some people love CSS as it is; others hate working with the global scope of the cascade. I’m not here to discuss CSS-in-JS (there are enough really smart people already talking about that), but locally scoped custom properties offer us a fantastic middle ground.
By taking advantage of locally scoped custom properties, we can create very modular code that takes a lot of the pain out of trying to come up with meaningful class names.
Let’s um, scope the scenario.
Part of the reason people get frustrated with CSS is that the following markup can cause problems when we want to style something.
<div class="card">
<h2 class="title">This is a card</h2>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Libero, totam.</p>
<button class="button">More info</button>
</div>
<div class="cta">
<h2 class="title">This is a call to action</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aliquid eveniet fugiat ratione repellendus ex optio, ipsum modi praesentium, saepe, quibusdam rem quaerat! Accusamus, saepe beatae!</p>
<button class="button">Buy now</button>
</div>
If I create a style for the .title
class, it will style both the elements containing the .card
and .cta
classes at the same time. We can use a compound selector (i.e. .card .title
), but that raises the specificity which can lead to less maintainability. Or, we can take a BEM approach and rename our .title
class to .card__title
and .cta__title
to isolate those elements a little more.
Locally scoped custom properties offer us a great solution though. We can apply them to the elements where they’ll be used:
.title {
color: var(--title-clr);
font-size: var(--title-fs);
}
.button {
background: var(--button-bg);
border: var(--button-border);
color: var(--button-text);
}
Then, we can control everything we need within their parent selectors, respectively:
.card {
--title-clr: #345;
--title-fs: 1.25rem;
--button-border: 0;
--button-bg: #333;
--button-text: white;
}
.cta {
--title-clr: #f30;
--title-fs: 2.5rem;
--button-border: 0;
--button-bg: #333;
--button-text: white;
}
Chances are, there are some defaults, or commonalities, between buttons or titles even when they are in different components. For that, we could build in fallbacks, or simply style those as we usually would.
.button {
/* Custom variables with default values */
border: var(--button-border, 0); /* Default: 0 */
background: var(--button-bg, #333); /* Default: #333 */
color: var(--button-text, white); /* Default: white */
/* Common styles every button will have */
padding: .5em 1.25em;
text-transform: uppercase;
letter-spacing: 1px;
}
We could even use calc()
to add a scale to our button, which would have the potential to remove the need for .btn-sm
, btn-lg
type classes (or it could be built into those classes, depending on the situation).
.button {
font-size: calc(var(--button-scale) * 1rem);
/* Multiply `--button-scale` by `1rem` to add unit */
}
.cta {
--button-scale: 1.5;
}
Here is a more in-depth look at all of this in action:
See the Pen
Custom list icons with CSS Custom Properties by Kevin (@kevinpowell)
on CodePen.
Notice in that example above that I have used some generic classes, such as .title
and .button
, which are styled with locally scoped properties (with the help of fallbacks). With those being setup with custom properties, I can define those locally within the parent selector, effectively giving each its own style without the need of an additional selector.
I also set up some pricing cards with modifier classes on them. Using the generic .pricing
class, I set everything up, and then using modifier classes, I redefined some of the properties, such as --text
, and --background
, without having to worry about using compound selectors or additional classes.
By working this way, it makes for very maintainable code. It’s easy to go in and change the color of a property if we need to, or even come in and create a completely new theme or style, like the rainbow variation of the pricing card in the example.
It takes a bit of foresight when initially setting everything up, but the payoff can be awesome. It might even seem counter-intuitive to how you are used to approaching styles, but next time you go to create a custom property, try keeping it defined locally if it doesn’t need to live globally, and you’ll start to see how useful it can be.
Definitely needed and useful from any point of view.
Glad you liked it Bojan!
looks gud. does it have ie11 support?
Custom properties aren’t supported in IE (sadly) – https://caniuse.com/#search=custom%20properties
but you could use PostCSS to take care of the backward compatibility.
Nope.
Worth to mention that custom properties should be used only when their values are dynamic and might be changed through js or custom CSS. If you don’t need runtime themes, you should not use custom properties but instead the sass-like variables that are build time only and don’t live in a browser
I guess there is behind-the-scenes reason to use :root:
Great article!
The more I know the more I know I don’t know. Or at least think that I know. It’s really good to read articles like this. So enlightening. Makes us rethink a lot of concepts.