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
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:
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");
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%
.