Vanilla CSS magic with custom properties

Undoubtedly, a critical factor for the giant leap we’ve seen in CSS development was the advent of pre/post-processors. Back then, when wonderful tools like PostCSS (and its great variety of plugins), Stylus, Sass and others were presented to the world, they allowed us to write our stylesheets in a completely different way. Elegant – and easier – code reuse, structuring, modularity and cleaning (e.g. Autoprefixer) were a really sexy sales pitch for those poor souls having to deal with huge-messy-nonDRY-monolithic stylesheets everyday. Moreover, elements of structured programming, like loops, conditionals and subroutines brought a whole new range of possibilities.

In the meantime, CSS was definitely not idle. Over the last years, it has been evolving steadily and at a very fast pace. It was truly a wise choice from the CSS Working Group to split the specification up into separate modules which level independently. This way of working enables a constant stream of new features being delivered to the development community. Now, we have Flexbox, CSS Grid Layout and other killer features. One of them is CSS variables (a.k.a. Custom Properties).

Custom properties VS Preprocessor variables

If you already have some experience with a CSS preprocessor, certainly you know the importance of variables. Let’s see a simple pen with an example in Stylus:

Preprocessor variables

As you probably already know, after the code above is compiled, the output CSS will lose the variables we’ve declared. Each reference to those variables will be replaced by its value, this way:

html {
font-size: 62.5%;
}
.content {
color: #af1b3f;
font-size: 1.4rem;
padding: 2rem;
}
.features-list {
display: flex;
list-style-type: none;
margin: 0 -0.5rem;
padding: 0;
}
.card {
background-color: #e0e0e0;
border-radius: 0.3rem;
flex-basis: 0;
flex-grow: 1;
margin: 0.5rem;
padding: 1rem;
}
.card__title {
margin: 0;
}

This is why they’re called preprocessors, ’cause everything different they’re supposed to do must be done before your code is delivered to the browser, during the compilation process, whose output is just plain CSS. In other words: your browser has no idea of what $base-color stands for.

The same result as the previous sample could be achieved by using vanilla CSS variables. Check out this new pen:

CSS Custom properties

So, what’s the big deal with CSS variables?

Well, they can be used in the same manner as the – plain old ? – Stylus/Sass/LESS variables, but they go beyond that:

Variable scope

In programming, variables are called this way because their values can change multiple times after they’re declared. It’s not different in CSS. Custom properties may change and, depending on where this change occurs, the scope of affected elements will vary.

The reason for initializing variables within the :root pseudo-class is because they will be usable by CSS rules matching any element in the whole document. In other words, the scope of a custom property is from the elements matched by the rule where it’s initialized to all their descendant nodes.

To make it clearer, let’s take a look at another example:

Custom properties scope

We’ve added a new card list with the .another-list class right after the first one. Then, we reassigned our variables in the line 24, inside a rule targeting this list’s class.

.another-list{
--some-other-color: white;
--base-color: #255F85;
}

By doing this, only elements with the .another-list class, along with all their descendants, will see the new values of these variables. Any node outside this hierarchical scope won’t be affected by this change. In fact, the original values still the same for the rest of the document. This can come in handy for a variety of needs (e.g. reassigning variables within media queries, for smarter responsiveness).

Another important aspect to be considered is the cascading effect. CSS stands for Cascading Style Sheets. In simple words, rules have different precedences, depending on factors like the specificity of selectors, order of declaration and use of the !important flag.

Custom properties are also affected by the cascade:

.another-list{
--some-other-color: red !important;
}
.content .another-list{
--some-other-color: blue;
}
.another-list{
--some-other-color: white;
}

The last rule has the lower precedence among the three. The second one, with a more specific selector, has a higher precedence. However, the first rule, with the !important declaration, wins this battle. After all, --some-other-color will be red.

Controllable by JavaScript

Well, one of the nicest things about CSS variables is that they’re really dynamic, so that you can control them with JavaScript.

Other usages

Let your imagination soar by using custom properties in varying ways:

  • Animations (Here’s a demo)
  • Assigning one variable value to another: --some-prop: var(--other-prop)
  • Playing with calc(): width: calc(var(--rectangle-width) * 2)
  • Setting a fallback value: color: var(--bgcolor, black)
  • You can pass variables through the style attribute too.

Conclusion

Custom properties have a decent support now. If you don’t need to worry about old browsers (i.e. IE), just dive right in. You can combine them with your favorite preprocessor. Have fun!