CSS Custom Properties AKA "variables"

This is about an upcoming feature that is currently usable in the latest Firefox, Chrome, and Opera. Support is notably missing in IE and Edge. Check Caniuse for details.

Many developers today use CSS pre-processors and if asked, would probably say they can't live without them. They can serve to reduce some of the work and clutter involved in making a large complex website. They can also do the opposite, however. General features of pre-processors include variables, the ability to include styles in one selector originating from another, functions, and the ability to nest selectors. The most used of these features seems to be variables. Perhaps as a result of this popularity, a spec was proposed to provide this feature within CSS itself.

The result is officially called Custom Properties and offers features similar to pre-processor variables but with an important and powerful distinction. This article will explore CSS custom properties, how they work, and their differences from LESS, a popular CSS pre-processor.

A quick comparison

Ok, first, let's start with the familiar. The following is a simple LESS variable used in a single selector, next to the transpiled output of the same:

@cerulean: #1dacd6;

h1 {
    background: @cerulean;
}
h1 {
    background: #1dacd6;
}

In CSS, the following box on the left is the formal syntax. The box on the right is the effect of this syntax, as CSS is interpreted directly.

:root {
    --cerulean: #1dacd6;
}

h1 {
    background: var(--cerulean);
}
h1 {
    background: #1dacd6;
}

The first thing to notice is the output is exactly the same. In the basic use-case of global variables, CSS custom properties behave exactly the same as LESS's variables. The second, and perhaps most obvious thing, is that CSS doesn't allow properties declared outside of any sort of selector. However the :root selector is sufficient to define properties accessible everywhere for reasons I'll cover in the next section. Finally, all properties must start with two dashes in order to be valid and can only be retrieved by way of the var function. The dashes are presumably to ensure they will never clash with any future spec-defined non-custom CSS properties.

Scoping

This is where I tell you I lied. I'm sorry. In the previous section I indicated that custom properties behave exactly the same as pre-processor variables in the basic case. This is somewhat untrue. To show you, let's take for example a few different placements of LESS's variables

h1 {
    @cerulean: #1dacd6;
    
    span {
        background: @cerulean;
    }
}
h1 {
    @cerulean: #1dacd6;
}

h1 span {
    background: @cerulean;
}

and the resulting CSS output:

h1 span {
    background: #1dacd6;
}
NameError: variable @cerulean is undefined

As you'll see in the first case, LESS has no problems sharing the variable with nested selectors. However if we split up up the selectors, LESS complains about its inability to find the variable we requested.

CSS on the other hand relies on the cascading (inheritance-based) nature of CSS to determine visibility of its custom properties. Since we cannot have nested selectors, we must settle for comparing only to the second example, shown on the left, and the effective behavior of this CSS, as shown on the right:

h1 {
    --cerulean: #1dacd6;
}

h1 span {
    background: var(--cerulean);
}
h1 {
}

h1 span {
    background: #1dacd6;
}

As the value is inherited, we can actually take this further and remove the h1 from the second selector and the result will behave exactly the same in the browser. LESS, and other pre-processors, would be unable to perform this level of variable resolution as their work finishes at transpilation time. This is what makes CSS custom properties quite powerful.

Pitfalls and Gotchas

What makes this syntax powerful can also be a detriment for the unaware. Someone used to pre-processors may be surprised at the result if they define a custom property in the "global" (:root) scope, and a custom property with the same name in another selector when a third selector using the property happens to match an element that would inherit from the second declaration. This may be a little confusing, so let's take a look at what this would look like in LESS, along with some markup exhibiting the issue in question and the resulting output:

@cerulean: #1dacd6;

h3 {
    @cerulean: blue;
}

span {
    color: @cerulean;
}

Some text in a title

More text in a paragraph.

Some text in a title

More text in a paragraph.

Using the following direct translation to CSS, however, we get a much different result as indicated by the output:

:root {
    --cerulean: #1dacd6;
}

h3 {
    --cerulean: blue;
}

span {
    color: var(--cerulean);
}

Some text in a title

More text in a paragraph.

Some text in a title

More text in a paragraph.

Here, the second declaration of "cerulean" redefines the value at the h3 point in the hierarchy. Any element in position to inherit property values from an h3 does so, and the result is as seen above. One way to avoid this would be to only define custom properties in :root, but this would make it much harder to manage many custom properties. This can also be avoided by judicious naming of properties to reduce the chance of unintentional redeclaration.

On the upside, we no longer need the "global" declaration if we only wish to target elements inheriting from an h3, as can be seen below:

h3 {
    --cerulean: #1dacd6;
}

span {
    color: var(--cerulean);
}

Some text in a title

More text in a paragraph.

Some text in a title

More text in a paragraph.

In this case, unlike LESS, CSS defaults all custom property values to nothing, causing them to be ignored when they are used. LESS requires declaration in the same or greater scope to use a variable and in this case would return an error as shown previously in this article.

Unintentional fallback to initial values

As var() is resolved at runtime it is considered by CSS parsers to be valid until the browser computes the actual value to be used, at which point it makes a final determination of validity. Normal properties with values that do not contain var() can be determined valid long before by simple syntax checking among other things. So what does this mean for us?

It means non-custom properties which have been declared more than once may not work as expected if they are followed with a value that contains var(). This is best exemplified by the following:

span {
    color: red;
    color: var(--cerulean);
}
some text we want to be colored
some text we want to be colored

So what happened? It was supposed to be red, right? Well, no. CSS started with the declaration of color with the value red, and determined it was valid. It then moved on to the declaration with var() and determined it was also valid, replacing the previous declaration. When it came time to compute the actual value, the browser found out the custom property we referenced did not exist and declared the entire value invalid. However at this point as it had already discarded the previous value, it was unable to fall back to it.

Complex use-cases and abilities

While CSS's custom properties may not be able to perform math on the declared values without using calc() or act as arguments to mix-ins, you can use them in a variety of property values as part of existing syntax. A quick example would be to use it to define a repeating linear gradient, which I demonstrate below.

:root {
    --spacing: 5px;
}

span {
    background: repeating-linear-gradient(-45deg, #1dacd6, #1dacd6 var(--spacing), transparent var(--spacing), transparent calc(var(--spacing) * 2));
}

More text in a paragraph.

More text in a paragraph.

In the above we could also replace the colors in the stops with var() and the browser would be quite fine with it. There is one caveat: there is no special case for the content property. Any value in a custom property must compute to valid syntax in order to show up. It does not auto-quotify them to display as text.

Default values

In a previous example in this article it was shown a browser will ignore the attempted use of a custom property which does not exist in the scope asking to use it. This is not always ideal. As such the var() function allows a default value to be specified which is used when the given custom property cannot be found. CSS considers any value after the first comma in a var() function call to be the default value. This means it can even include more commas.

span {
    color: var(--cerulean, lightblue);
}

JavaScript access (CSSOM)

Finally, the second killer ability of CSS custom properties is their ability to be updated on the fly. Traditional pre-processors only perform transpiling once, baking in all variable values for the entire runtime of the page. The spec on the other hand gives us a way to update the value of these properties ourselves, and the change will be reflected on the page. This could prove to be quite handy. The following contrived example, using the gradient example's styles and markup, indicates how to do this.

document.querySelector(":root").style.setProperty("--spacing", "3px");

More text in a paragraph.

Next, the spec is quite permissive in what it allows for the value of custom properties. Their stated intent for this inclusiveness is to enable potential future uses via JavaScript for those values, even if they are not further used within the stylesheet. An example of retrieving property values is given below. Be aware the returned value may include all text after the colon but before the semicolon, including any leading spaces. Values set by JavaScript won't have any leading spaces unless explicitly included in the value.

span {
    --highlights: 2 5, 4 10, 6 8, 20 35;
}
var highlights = window.getComputedStyle(document.querySelector("span")).getPropertyValue("--highlights");

I don't have a bunch of great examples on hand about how to go about using JS access usefully, but it might be interesting to try using it as a simple user theme generator. If some UI exposed all reasonable custom properties, they could be updated on the fly by JavaScript without having to write out a new custom stylesheet per-user. To see that and other things from this article in action, check the demo page.

And that's it! Thank you for reading.

July Addendum: Another usage for JS updates

After originally writing this article, I eventually did find another interesting use-case for updating custom properties in JavaScript: CSS triangles used with message balloons. Message balloons are used in many places and frequently they have a triangle that points to the item they relate to. These triangles can be done with :before or :after pseudo-elements, at a fixed location. Such a balloon example is shown below:

Some text here.

This requires the balloon to move to keep the triangle next to or in the middle of the relevant element. Sometimes, however, it would be nicer if the triangle could move when the balloon cannot. This may happen in cases where the relevant element is close to the edge of the page or viewport and you do not wish to cut off the balloon. Unfortunately pseudo-elements are untouchable from JavaScript and we cannot change their styles on the fly. Custom properties are able to be updated by JavaScript and so can come to our rescue.

document.querySelector(".dynaballoon")
        .style.setProperty("--after", "15px");
Some text here.

This works great, but do remember the section from above about the processing order of var. In browsers that don't recognize this new feature, the value 50% will be used. Those which do recognize it will throw out that "backup" value as var is considered to be valid at processing/parsing time. Therefore any invalid values passed by JavaScript will cause all properties where it is used to revert to their initial values, not 50%.