insights | 24.11.2017

CSS Custom Properties

CSS custom Properties

As a design agency, we like to keep up-to-date with the latest advances in the languages we use. One of the more interesting additions to a language is CSS custom properties (sometimes called “CSS variables”). In this article, we’ll look at what they are and how to use them.

What is a CSS Custom Property?

To understand CSS custom properties, perhaps we should explain normal CSS properties.

CSS rules are made up of various parts. For example, consider the following piece of CSS code:

#one {
    color: red;
}

The CSS snippet above shows a rule set with a selector (#one) containing a single declaration (color: red;) – that declaration’s property is color and the value is red. (Thank you Impressive Webs for the terminology.) CSS has known properties and will ignore any declaration with a property that isn’t recognised or value that isn’t valid. CSS custom properties are essentially a namespace that allow us to create our own properties which CSS will remember even if it doesn’t understand them.

In the example below, we create a CSS variable called “colour” and give it the value “red”.

#two {
    --colour: red;
}

CSS doesn’t understand the custom property – we need to use it on a known property which we can with the var() function (short for “CSS variable”). We can use the value is a variety of different ways.

/**
 * 1. var() can be the whole value for the declaration.
 * 2. var() can also be used as part of the value.
 * 3. var() can even be used in other functions.
 */
#two {

    --colour: red;
    --lines: 2;

    color: var(--colour);              /* [1] */
    border: 1px solid var(--colour);   /* [2] */
    height: calc(30px * var(--lines)); /* [3] */

}

The var() function also allows us to define a fall-back value, in-case the CSS custom property we defined hasn’t been set. We can even nest var() calls, allowing the fall-back to be another CSS variable.

/**
 * 1. We can provide a value a the fall-back.
 * 2. We can nest var() functions so the fall-back is another CSS custom
 *    property. That fall-back can have a fall-back of its own - there's no
 *    limit to the nesting.
 */
#three {

    --colour-1: red;
    --colour-2: blue;

    color: var(--colour-1, green);                  /* [1] */
    border-color: var(--colour-3, var(--colour-2)); /* [2] */

}

Any child element of #three has access to the --colour-1 CSS custom property. To better understand that, we should explain scope.

Scope

In JavaScript, internal functions can see the variables from outer functions or override them but outer functions cannot see inner function variables.

var letter = "a";
var number = 1;
console.log("Global scope, letter is %s and number is %d", letter, number);

function outerFunction() {

    var letter = "b";

    function innerFunction() {

        var number = 2;
        console.log(
            "innerFunction, letter is %s and number is %d",
            letter,
            number
        );

    }

    console.log("outerFunction, letter is %s and number is %d", letter, number);
    innerFunction();

}

outerFunction();

// Logs:
// Global scope, letter is a and number is 1
// outerFunction, letter is b and number is 1
// innerFunction, letter is b and number is 2

CSS variables work in a very similar way – inner elements can see outer element properties but outer elements cannot see inner element variables. (The :root selector is an alias for html but with a stronger specificity – we use it to contain global CSS variables.)

:root {
    --letter: "a";
    --number: "1";
    color: red;
}

.outer {
    --letter: "b";
    color: green;
}

.inner {
    --number: "2";
    color: blue;
}

:root::before,
.outer::before,
.inner::before {
    content: "letter is " var(--letter) " and number is " var(--number);
}

The only difference is that this is based on HTML structure rather than any kind of nesting.

<div class="outer">
    <div class="inner"></div>
</div>
<!-- Generates:
letter is a and number is 1
letter is b and number is 1
letter is b and number is 2
-->

<div class="outer"></div>
<div class="inner"></div>
<!-- Generates:
letter is a and number is 1
letter is b and number is 1
letter is a and number is 2
-->

The way that HTML structure can change the value of CSS custom properties can seem confusing at first glance, but it offers incredible flexibility when it comes to creating variations. For example, consider Bootstrap’s panels. With CSS custom properties, the main .panel element will have a list of custom properties which sub elements will use.

.panel {

    --panel-border-colour: #DDD;
    --panel-border-radius: 0.5em;
    --panel-border-width: 1px;
    --panel-theme-background: #F5F5F5;
    --panel-title-colour: var(--body-colour);

    border-color: var(--panel-border-colour);
    border-radius: var(--panel-border-radius);
    border-style: solid;
    border-width: var(--panel-border-width);
    margin-bottom: 1em;

}

/* ... */

.panel__header,
.panel__footer {
    background-color: var(--panel-theme-background);
}

.panel__title {
    color: var(--panel-title-colour);
}

/* ... */

The variant classes just need to adjust the CSS custom properties – the sub elements will automatically be updated as a result.

.panel--success {
    --panel-theme-background: #dff0d8;
    --panel-border-colour:  #d6e9c6;
    --panel-title-colour: #3c763d;
}

Manipulating CSS Custom Properties with JavaScript

Since CSS custom properties are just regular CSS properties, they can be manipulated with JavaScript the same as any other. An article on Smashing Magazine shows a couple of useful functions for reading and writing CSS custom properties.

function readCssVar(element, varName) {

    return window
        .getComputedStyle(element)
        .getPropertyValue("--" + varName)
        .trim();

}

function writeCssVar(element, varName, value) {
    return element.style.setProperty("--" + varName, value);
}

Combining those functions and knowledge of scope, we can create a simple colour picker.

Currently jQuery can’t handle CSS custom properties in its .css() function but it looks like it will be possible in a future version.

// Doesn't work at time of writing, but it could work in the future.
$("#two").css("--colour", "red");

The difference between CSS custom properties and CSS variables

You may have noticed both phrases being used so far. According to the specs, the correct name is “CSS custom properties” but “CSS variables” is a common name. Both phrases refer to the same thing and we’ll use both interchangeably throughout this article.

Advantages

Setting CSS custom properties has many advantages over setting inline styles.

1. We keep the style information together.

It’s very common to have a small number of set media query sizes in a style sheet but it’s also very common to need to execute JavaScript when those media queries trigger. Previously, we had to keep the media query sizes in CSS and JavaScript, remembering to update both if we changed one. By keeping the values in a single place, we only have to update the code once.

:root {
    --screen-xs-min: 480px;
    --screen-sm-min: 768px;
    --screen-md-min: 960px;
    --screen-lg-min: 1200px;
}
var SIZES = {};
SIZES.xsMin = readCssVar(document.documentElement, "screen-xs-min");
// ...

We can now use those values in our JavaScript.

window
    .matchMedia("(min-width:" + SIZES.mdMin + ")")
    .addListener(function (match) {
        // ...
    });

2. We can separate styles from scripts.

The styles can interpret the CSS custom properties however they want. This allows us to change the styles without necessarily having to change the scripts.

The style in this example must be a background colour, the value can’t be used anywhere else.

<div style="background-color: #F00;"></div>

What if the theme gets updated so we need to use a gradient or set the font-colour? Compare that to the markup below:

<div style="--theme-colour: #F00;"></div>

Now our styles can decide where and how to use the value. We can use it as often as we like, combine it with gradient syntax or even ignore it completely. Crucially: the changes we make to the styles’ interpretation of that property doesn’t need to change the script that sets it.

3. The specificity doesn’t need to be changed.

Inline styles can cause a lot of problems when it comes to creating responsive layouts. A common use for inline styles is setting the height of one element to match the height of another. This used to leave us markup like this:

<div class="box" style="height: 150px;"></div>
<div class="box" style="height: 150px;"></div>

However, that creates a problem when the screen size increases. The only way that we can override the inline style is to use !important and the only way to override !important is with another !important – this is why I consider !important to be toxic and always worth avoiding.

Our boxes could end up having a style like this.

@media (min-width: 768px) {
    .box {
        height: 300px !important;
    }
}

While that looks simple enough and it would override the inline style, imagine if we had another box that needed a set height.

<div class="box" style="height: 150px;"></div>
<div class="box" style="height: 150px;"></div>
<div class="box box--set-height" style="height: 150px;"></div>
.box--set-height {
    height: 500px;
}

We have no choice, we have to include an !important in the height declaration of .box--set-height if we want that element to be the height we desire. Consider the alternative of setting a CSS custom property.

<div class="box" style="--height: 150px;"></div>
<div class="box" style="--height: 150px;"></div>
<div class="box box--set-height" style="--height: 150px;"></div>
.box {
    height: var(--height, auto);
}

@media (min-width: 768px) {
    .box {
        height: 300px;
    }
}

.box--set-height {
    height: 500px;
}

By avoiding !important, we keep our specificity low and our CSS selectors fast. We would still need !important to override the value of a CSS custom property that’s been set inline like the example above, but we don’t need !important to ignore that property.

Gotchas

They can’t be used in media queries.

Unfortunately, we can’t use CSS custom properties in “at rules” like a media query. Our raw CSS would still need to have the media queries manually written.

/* This will not work */
@media (min-width: var(--screen-xs-min)) {
}

/* This will work */
@media (min-width: 480px) {
}

Adding a CSS custom property into a media query can only be done with the aid of a pre-processor. Currently, there is no way of naming a media query in CSS alone. Hopefully that will change one day.

Browser support is patchy.

The support for CSS variables is pretty, but not good enough for a production environment yet. According to Can I Use: Chrome, Firefox, Safari and Opera handle CSS custom properties without a problem but Internet Explorer (even IE Edge 16) still has buggy support. Hopefully the bugs will be resolved in a future version and we can take advantage of the functionality mentioned in this article.

There’s no type coercion.

Unlike JavaScript, CSS variables will not get any type coercion. This means that you have to use colour literals where you mean to and you have to convert units using calc().

/**
 * 1. As a string, the hex code will not be recognised.
 * 2. As a colour literal, the CSS custom property will work as expected.
 */
.literals {

    --will-fail: "#F00";
    --will-succeed: #F00;

    border-color: var(--will-fail); /* [1] */
    color: var(--will-succeed);     /* [2] */

}

/**
 * 1. Without units, CSS won't understand the value.
 * 2. Using calc() we can convert the unit into pixels to create a valid value.
 */
.units {

    --size: 10;

    border-width: var(--size);          /* [1] */
    font-size: calc(var(--size) * 1px); /* [2] */

}

In our scope example, you may have noticed that both the --number and --letter CSS properties were strings. Had --number have been a number, it wouldn’t have appeared in the content.

Conclusion

CSS custom properties present an exciting enhancement to CSS and they can be used in personal projects. They offer better coding patterns for transferring information between CSS and JavaScript as well as creating a simple way of creating variations in CSS components and passing themes.

Browser support can only improve so we hope you’ve enjoyed a look at the future of CSS.

Want to view some of our work and read some more blogs? Check it out here

go back Back