CSS Custom Properties

Introduction

In the last few years CSS preprocessors had a lot of success. It was very common for greenfield projects to start with Less or Sass. And it’s still a very popular technology.

The main benefits of those technologies are, in my opinion:

  • They allow to nest selectors
  • The provide an easy imports functionality
  • They give you variables

Modern CSS has a new powerful feature called CSS Custom Properties, also commonly known as CSS Variables.

CSS is not a programming language like JavaScript, Python, PHP, Ruby or Go where variables are key to do something useful. CSS is very limited in what it can do, and it’s mainly a declarative syntax to tell browsers how they should display an HTML page.

But a variable is a variable: a name that refers to a value, and variables in CSS helps reduce repetition and inconsistencies in your CSS, by centralizing the values definition.

And it introduces a unique feature that CSS preprocessors won’t never have: you can access and change the value of a CSS Variable programmatically using JavaScript.

The basics of using variables

A CSS Variable is defined with a special syntax, prepending two dashes to a name (--variable-name), then a colon and a value. Like this:

:root {
  --primary-color: yellow;
}

(more on :root later)

You can access the variable value using var():

p {
  color: var(--primary-color)
}

The variable value can be any valid CSS value, for example:

:root {
  --default-padding: 30px 30px 20px 20px;
  --default-color: red;
  --default-background: #fff;
}

Create variables inside any element

CSS Variables can be defined inside any element. Some examples:

:root {
  --default-color: red;
}

body {
  --default-color: red;
}

main {
  --default-color: red;
}

p {
  --default-color: red;
}

span {
  --default-color: red;
}

a:hover {
  --default-color: red;
}

What changes in those different examples is the scope.

Variables scope

Adding variables to a selector makes them available to all the children of it.

In the example above you saw the use of :root when defining a CSS variable:

:root {
  --primary-color: yellow;
}

:root is a CSS pseudo-class that identifies the document, so adding a variable to :root makes it available to all the elements in the page.

It’s just like targeting the html element, except that :root has higher specificity (takes priority).

If you add a variable inside a .container selector, it’s only going to be available to children of .container:

.container {
  --secondary-color: yellow;
}

and using it outside of this element is not going to work.

Variables can be reassigned:

:root {
  --primary-color: yellow;
}

.container {
  --primary-color: blue;
}

Outside .container, --primary-color will be yellow, but inside it will be blue.

You can also assign or overwrite a variable inside the HTML using inline styles:

<main style="--primary-color: orange;">
  <!-- ... -->
</main>

CSS Variables follow the normal CSS cascading rules, with precedence set according to specificity

Interacting with a CSS Variable value using JavaScript

The coolest thing with CSS Variables is the ability to access and edit them using JavaScript.

Here’s how you set a variable value using plain JavaScript:

const element = document.getElementById('my-element')
element.style.setProperty('--variable-name', 'a-value')

This code below can be used to access a variable value instead, in case the variable is defined on :root:

const styles = getComputedStyle(document.documentElement)
const value = String(styles.getPropertyValue('--variable-name')).trim()

Or, to get the style applied to a specific element, in case of variables set with a different scope:

const element = document.getElementById('my-element')
const styles = getComputedStyle(element)
const value = String(styles.getPropertyValue('--variable-name')).trim()

Handling invalid values

If a variable is assigned to a property which does not accept the variable value, it’s considered invalid.

For example you might pass a pixel value to a position property, or a rem value to a color property.

In this case the line is considered invalid and ignored.

Browser support

Browser support for CSS Variables is very good, according to Can I Use.

CSS Variables are here to stay, and you can use them today if you don’t need to support Internet Explorer and old versions of the other browsers.

If you need to support older browsers you can use libraries like PostCSS or Myth, but you’ll lose the ability to interact with variables via JavaScript or the Browser Developer Tools, as they are transpiled to good old variable-less CSS (and as such, you lose most of the power of CSS Variables).

CSS Variables are case sensitive

This variable:

--width: 100px;

is different than:

--Width: 100px;

Math in CSS Variables

To do math in CSS Variables, you need to use calc(), for example:

:root {
  --default-left-padding: calc(10px * 2);
}

Media queries with CSS Variables

Nothing special here. CSS Variables normally apply to media queries:

body {
  --width: 500px;
}

@media screen and (max-width: 1000px) and (min-width: 700px) {
  --width: 800px;
}

.container {
  width: var(--width);
}

Setting a fallback value for var()

var() accepts a second parameter, which is the default fallback value when the variable value is not set:

.container {
  margin: var(--default-margin, 30px);
}