Getting started with CSS Variables | Heart Internet Blog – Focusing on all aspects of the web

The year 1991 became a turning point for the web. Back then Tim Berners-Lee first announced HTML. At that time there was no method for styling web pages.

A five-year period went by before the first official CSS Level 1 specification was published by Håkon W. Lie and Bert Bos.

I started developing apps and sites for the web only six years ago, and 1996 seems so far away and removed from my reality as a CSS author. But there is one concurring issue: the need for performant techniques to overcome some of the DOM and CSS shortcomings.

Probably like most of you, I started learning plain vanilla CSS, without any pre-processors involved in the process. Those were the times of learning the syntax and most importantly the specs. Remember, CSS is all about knowing your specs!

Applications got bigger, CSS became complex and hard to maintain. This is when pre-processors like Sass and Less came along. They promised a better way to write and maintain CSS, with a terse syntax and a feature set of variables, functions, conditions, and loops. This has been the way for us to write better CSS (or worse, if you are not looking after your compiled CSS result!).

Compiler variables have been very useful to all of us. They gave us the ability to store values like colours, spacing and font preferences in one place. We could later reference them in multiple other places throughout our document during compilation time. They also allowed us to assign names to property values giving them meaning.

Now there is a native way to declare and reference variables, known as CSS Custom Properties. But Custom Properties are much more than a method to store and reuse values. It’s actually a game changer that will influence the way we write our CSS. In this article, we will cover 10 things you need to know about Custom Properties a.k.a CSS Variables.

The syntax

A glimpse at the syntax can be somehow awkward at first, especially if you are used to the $my-variable or @my-variable notations. But there are a lot of meaningful decisions behind the current syntax.

Declaration

A custom property is a property that starts with a hyphen-minus prefix that looks just like this: --*. CSS doesn’t give any meaning to the property names besides the value represented in them. For now, we can declare the colour values of our hypothetical CI palette.

:root {
      --hex-carmin-pink: #E94E3E;
      --hex-cyber-grape: #584C84;
      --hex-lilac: #C3A0BE;
      --hex-slate-blue: #6765CD;
      --hex-st-patricks-blue: #232467;
}

Properties

In the example above you may have noticed that we are using the :root pseudo-class. This selector has been around for a while and was defined in the CSS Selectors Level 3 specification. It refers to the &;lt;html> element of the web page.

There’s a slight difference between the pseudo-class and the HTML element: :root has a higher specificity. The :root pseudo-class can be very useful for declaring global custom properties. More about the global part will be covered below.

Values

Nearly every single value is valid when you assign it to a custom property. The reasons for this is that when the browser parses the custom property it doesn’t know where they will be used.
For example --script: x > 5 ? 10 : 0; is a valid custom property value. As a CSS property value, it would be useless because it’s invalid but this value can be of interest to JavaScript.

One thing to note is that the declaration is only invalid at computed-value time. They can’t fail any earlier because by the time the browser realises a property value is invalid, it already has thrown away the other cascaded values. The CSS declaration itself is invalid at computed-value time. This is an important acknowledgment for developers, especially if you don’t want to spend hours debugging your CSS.

Referencing

By using the var() function we can access the values we previously assigned at the root level. This is how the browser will substitute and insert the assigned value. The var() function can be used only as property values. Anything else would usually produce an invalid syntax error.

Now, let’s say we have an SVG <circle class="node"> and we would like to use one of our defined colours as a fill.

.node {
      fill: var(--hex-carmin-pink);
}

A nice feature of the var() function is that you can declare a fallback value as a second argument of the function.

.node {
      fill: var(--hex-carmin-pink, pink);
}

It also accepts the reference to another custom property as long as it’s wrapped in its own var() function.

.node {
      fill: var(--hex-carmin-pink, var(--hex-lilac));
}

Case sensitivity

When I started using custom properties about two years ago, I didn’t know that CSS is case insensitive.

/* All of these rule sets are valid CSS */
element {
      background-color: papayawhip;
      Background-Color: PapayaWhip;
      background-Color: Papayawhip;
}

But with custom properties it’s an entirely different story. Custom properties declarations are indeed set with the case-sensitive flag.

/* Both custom properties contain the same value /
/ but they are two different properties */
:root {
      --hex-carmin-pink: #E94E3E;
      --hex-Carmin-Pink: #E94E3E;
}

Scope: rules and inheritance

According to the spec, custom properties are ordinary CSS properties. This means that they are resolved with normal inheritance and cascade rules.

This is a very important point. We should use this information to our advantage and draft several application methods according to our project requirements.

Component-like encapsulation

CSS isn’t that hard, but as soon as projects start to scale, CSS gets a life of its own. In the attempt to bring sanity to CSS authors a lot of methodologies came out; BEM, SMACSS, Atomic Design, you name it. There are a ton of them, and they have been very useful to me over the years.

The very nature of the cascade gets in our way. It’s a dilemma, don’t you think? Even if we meticulously write CSS, we will encounter some inheritance and specificity problems somewhere along the way. That’s one of the reasons we are experiencing new alternatives approaches like the current CSS-in-JS boom.

Custom properties have a declaration scope. Think of it like JavaScript let statements that declare block-scoped variables. Style rules living within the scope they were declared allow us to write modular CSS. This is ideal for component-based development where encapsulation and composition are very important concepts. It can really improve the development of living style guides too.

Just by adopting this approach we automatically improve the specificity and semantics of our CSS. We will have much more control of the cascade and avoid style rules leaking. Usually, we should declare variables in a declaration scope unless we really need global variables.

/**
* We declared all the necessary styles
* for the button within the .button
* declaration.
*/
.button {
      /**
      * Values that need to be adjusted according
      * to the modification type are declared as
      * custom properties.
      */
      --hex-btn-bg: var(--hex-celestial-blue);
      --hex-btn-color: var(--hex-floral-white);
      --btn-radius: 5;
      --btn-height: 80;
      box-sizing: border-box;
      width: 320px;
      height: calc(var(--btn-height) * 1px);
      border: 3px solid transparent;
      border-radius: calc(var(--btn-radius) * 1px);
      margin-bottom: 24px;
      font-size: 16px;
      color: var(--hex-btn-color);
      letter-spacing: 2px;
      text-transform: uppercase;
      background-color: var(--hex-btn-bg);
      /**
      * But instead of overwriting the rule declaration
      * we can reassign the same custom property with a
      * new value in an element with a higher specificity.
      */
      &.button--secondary {
           --hex-btn-bg: var(--hex-independence);
      }
      &.button--highlight {
           --hex-btn-bg: var(--hex-princeton-orange);
      }
      &.button--invert {
           --hex-btn-bg: transparent;
           --hex-btn-color: var(--hex-princeton-orange);
           --btn-radius: 40;
           --btn-height: 60;
           border-color: var(--hex-princeton-orange);
      }
}

See it in action on CodePen.io.

Global declarations

There are other times we really need or want to take advantage of the cascade and the CSS inheritance flow. Declaring customs properties at a top level allows these values to flow through all of their children. For example, by using the :root pseudo-class as a declaration element we have access to these values across our document. Think of it like declaring variables in JavaScript at a global scope.

One word of caution: manipulating these custom properties at a top|root level can cause style re-calculations, triggering layout and repaint throughout the DOM. In that case, try to use CSS properties that don’t trigger layout operations when you manipulate their values at :root level. For example, you can use a property like transform that doesn’t trigger any layout or re-painting, which is actually what you want. It means that the operation can likely be carried by the compositor thread with some GPU assistance.

Global declarations can be extremely useful for localisation, theme switching or even combining them both.

/**
* We define the direction of text, table columns,
* and horizontal overflow according to the document
* language. Just by defining the lang custom propery in
* root we chan switched the entire document.
*/
:root:lang(en) {
      --direction: ltr;
}
:root:lang(ar) {
      --direction: rtl;
}
:root {
      direction: var(--direction);
}
.teaser {
      --hex-aero: #72B5EC;
      --hex-independence: #4B5160;
      --hex-navajo-white: #FFDFA6;
      --space-s: 14;
      --space-m: 28;
      --space-l: 56;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      margin: 0 auto calc(var(--space-l) * 1px);
      color: var(--hex-navajo-white);
      font-family: 'BLOKK';
      @media(min-width: 740px) {
           max-width: 1024px;
      }
      .headline {
           margin: 0 0 calc(var(--space-m) * 1px);
           font-size: 32px;
           letter-spacing: 4px;
      }
      .copy {
           line-height: 1.5;
      }
}
.teaser-content {
      box-sizing: border-box;
      max-width: 60%;
      &:lang(en) {
           &:not(:first-child) {
                margin-left: 30px;
           }
      }
      &:lang(ar) {
           &:not(:first-child) {
                margin-right: 30px;
           }
      }
}

See it in action on Codepen.io.

Conditional

In the above pen, we saw that by modifying a custom property we could change the language direction of the entire document.

Now do you remember we mentioned something like the var() function accepting a fallback value? We can use this feature to our advantage to create a conditional-like behaviour. In the example below we’ll change an entire theme just by defining the value of an unset custom property. Let’s take a look at the code.

.chrome-canvas {
      /**
      * At this point we are telling our --themes
      * custom property that we have two different themes:
      * 1. --light
      * 2. --dark
      */
      --themes: var(--light, var(--dark));
      --dark: var(--light, var(--hex-black-coral));
      --theme-interval: 1000;
      min-height: calc(100vh - (var(--header-height) * 1px));
      padding: 20px;
      background-color: var(--themes);
      transition: background calc(var(--theme-interval) * 1ms);
      @media(min-width: 740px) {
           padding: 40px;
      }
}
/**
* When the input checkbox is :checked
* we assign a value to our light theme
* that was `unset` before;
*/
.chrome-state {
      &:checked ~ .chrome-canvas {
           --light: var(--hex-floral-white);
           .teaser,
           .chrome-btn {
                --light: var(--hex-black-coral);
           }
           .teaser-media {
                --light: var(--hex-princeton-orange);
           }
      }
}

See this in action on Codepen.io.

What about real conditions? I came across an article from Roman Komarov about using 0 and 1 to switch values inline. It’s a really useful technique and I have already used it in projects.
You can use this technique to switch any value you need from spaces to colours. Let’s take a look at his example:

Available at runtime

This is by far the most powerful custom properties feature: custom properties are available at runtime. Think about it for a moment, it’s absolutely mind-blowing!

I’ve spent countless hours trying to to find performant techniques to manipulate the quirkiness of the DOM. Researching all kinds of techniques to generate smooth animations, trying desperately to get CSS to work smoothly with JavaScript.

Custom properties are the new alliance between CSS and Javascript. Why? Because they are accessible to JavaScript at all times.

const space = () => {
      const root = document.querySelector(':root');
      const translation = {
           time: 0,
           step: 0.01
      };
      const angle = {
           sin: 0,
           cos: 0
      };
      const update = () => {
           translation.time = (translation.time + translation.step) % 1;
           angle.sin = Math.sin(translation.time * Math.PI * 2);
           angle.cos = Math.cos(translation.time * Math.PI * 2);
           root.style.setProperty('--sin', angle.sin);
           root.style.setProperty('--cos', angle.cos);
           requestAnimationFrame(update);
      };
      update();
};
space();

In the example above, we prepare the calculations necessary to animate a ball in a circular motion. By running a looping timeline value starting at 0 and ending at 1, defining a timeline for sine and cosine. In our update() function we set the values in our DOM.


const space = () => {
      const root = document.querySelector(':root');
      const translation = {
           time: 0,
           step: 0.01
      };
      const angle = {
           sin: 0,
           cos: 0
      };
      const update = () => {
           translation.time = (translation.time + translation.step) % 1;
           angle.sin = Math.sin(translation.time * Math.PI * 2);
           angle.cos = Math.cos(translation.time * Math.PI * 2);
           root.style.setProperty('--sin', angle.sin);
           root.style.setProperty('--cos', angle.cos);
           requestAnimationFrame(update);
      };
      update();
};
space();

See this in action on Codepen.io.

This is just the tip of the iceberg of all the things you can do with custom properties and JavaScript. Now it’s time to start using custom properties and experiment with new and interesting techniques.

Browser support

Custom properties are currently supported in most of today’s browsers, as we can see in the image below. Only IE will not support this feature at all. Nevertheless, it’s currently supported in Edge. In my opinion, the browser support is pretty decent and production ready. We have already been using CSS Variables in several projects.

The @supports property

We now have a way to check natively for feature support in the browser. The CSS feature detection days with Modernizr are something of the past. I get so excited every single time I say that!

With the @supports CSS at-rule, we can verify directly in CSS if the browser supports custom properties. The rule can be declared at a :root level in your CSS or within a declaration block.

/* supported */
@supports (--direction: ltr) {}
/* NOT supported */
@supports (not (--direction: ltr)) {}

There are many alternatives on how to check for custom properties browser support that are out of the scope of this article. The important thing to remember is that with custom properties you can progressively enhance your application in a very elegant way today.

References and further reading

Comments

Please remember that all comments are moderated and any links you paste in your comment will remain as plain text. If your comment looks like spam it will be deleted. We're looking forward to answering your questions and hearing your comments and opinions!

Got a question? Explore our Support Database. Start a live chat*.
Or log in to raise a ticket for support.
*Please note: you will need to accept cookies to see and use our live chat service