Playing around with CSS variables - "custom properties"

Variables have been a much wished-for addition to CSS for a long time. Yesterday, Mozilla was the first browser maker to add support for them, in its latest Firefox Nightly. I played around with it for a bit, trying to come up with uses for them, apart from the most obvious (making colors, font-sizes etc more DRY). These thoughts are very unfinished, and possibly stupid. But hey, I guess it'll be a while before we see enough browser support for these to make them useful, so we have time to iron out the kinks.

To begin with, the name CSS variables is only partly correct. They are variables of a sort, but the correct name, and way of looking at it, is custom properties. The full name of the specification is "CSS Custom Properties for Cascading Variables". I have a hunch that the "Cascading Variables" part of the name is what makes it really interesting.

The basics

Let's jump into some introductory examples. To create a custom property, you simply declare it, and make sure it begins with var-, much like data- attributes on HTML elements. The value can be any valid value for CSS. So to create a custom property (or variable, if you wish) that we can use anywhere in our document, we declare it using the :root selector:

:root {
    var-primary-color:  firebrick; /* yes, that is a valid color keyword. pret-ty cool. */
}

To use this variable somewhere else, you can reference it with the var() function notation, passing in the property name sans the var- bit (again, a bit like data- and the element.dataset API, but without camelCasing). The second argument to the function is the fallback value, in case the custom property is not declared on an ancestor (and thus inherited) or invalid:

.myComponent {
    color: var(primary-color, #000);
}

Using the cascade as variable scope

Declaring a custom property only on a specific selector means that it's only reachable within the context of that selector, since it's automatically inherited. Combining this with the fact that using custom properties can have fallback values, we can decouple specific styling from selectors, and couple them to the context of the variable instead.

One example that pops up in my head is when we have a part of a page that comes from a CMS, like a blog post: in that case, we might want to apply some sort of base styling that we don't really use outside heavily of this context. The goal is to make the HTML inside of this block behave consistently, and not let that affect the rest of the page (which might be more "UI" than "content", if you get what I mean). I usually have some sort of classname, like .flow, that I use in these cases, and then style descendant selectors.

.flow p,
.flow h2,
.flow h3,
.flow h4,
.flow ul {
    /* Properties for nice consistent flow here */
}

Now, say that this styling relies on some sort of vertical rythm, sort of like Harry Roberts’ idea of single-direction margin directions. We can create a context where this is applied to descendants of the .flow class without really tying it to the selector itself, but instead tying it to the base styling of the elements themselves. An example to explain:

.flow {
    /* set the var-flow-space custom property on elements inside .flow */
    var-flow-space: 1.375rem;
}
h2, h3, h4, h5, ul, p {
    /* If the var-flow-space is defined, apply that for margin bottom, otherwise 0 */ 
    margin: 0 0 var(flow-space, 0) 0 ;
}
}

This means that outside of the “flow” context, elements are normalized to have no margin, except margin-bottom if the var-flow-space property is inherited. We could, if we wish, define other contexts where we want to use a consistent margin-value for these elements, e.g. small-print: we just make sure to redefine the variable inside a selector that we use for that context.

Going further: goodies from the spec

One of the first things that I tried (without reading up on wether it was possible or not) was to concatenate/interpolate other property values with the var() notation, like var(my-val)px. This does not work. However, the spec mentions this case explicitly, and gives us a brilliant use of the calc() function to make this happen. We can simply create the units we want by operating on them:

:root {
    /* unitless number */
    var-vertical-base: 1.375;
}
.flow {
    /* use the number to set context-specific base number */
    var-flow-space: var(vertical-base);
}
h2, h3, h4, h5, ul, ol, p, blockquote {
      /* Normalize, use calc to give the flow-space (if set) variable a unit */
      margin: 0 0 calc(var(flow-space, 0) * 1rem) 0;
}
blockquote {
    /* give blockquotes inside contexts where flow-space is set a consistent side padding */
    padding: 0 calc(var(flow-space, 0) * .5rem);
}

Setting custom properties to read from scripts

Another thing that the spec mentions explicitly is setting custom properties to be read by JavaScript. This ties in nicely with the idea of not having breakpoints duplicated in both CSS and JavaScript, like what Jeremy outlined in “Conditional CSS” and that I (and others) proposed a solution to via using the content property alongside getComputedStyle.

Sadly, it does not seem possible to use custom property values inside media queries, like screen and (min-width: var(lapsize)), which would possibly make authoring of media queries a bit more DRY. It is, however, fully valid to set variable values inside media queries, so we could do something like this:

/* Yes, I know the category names are convoluted, but man, these things are hard. */
@media screen and (min-width: 25em) {
    :root {
        var-screen-category: small-lap;
    }
}
@media screen and (min-width: 48em) {
    :root {
        var-screen-category: lap;
    }
}
@media screen and (min-width: 80em) {
    :root {
        var-screen-category: desk;
    }
}

Then we can read these values by using the new JS API to get the value of custom properties, using something like this on resize etc:


var screenCategory = el.style.var.get(‘screen-category’);

This is not implemented in the Firefox Nightly, so I haven’t been able to play around with it yet.

I’ve put up the very quick-n-dirty source code for my experiments on JSBin. Download the nightly, play away!

Like I said in the beginning of the article, it’ll probably be a while before this is in all major browsers, and using it properly without massive fallbacks (thus negating the need for it) would be hard. I’m not sure how hard or easy it would be to polyfill in some way (probably hard), but for now it’s just massively fun to play with and try to figure out how to use once it’s there.