Over the last few years, CSS preprocessors have been very successful. Greenfield projects often started with Less or Sass — and they are still popular.

In my view, their main advantages are:

  • Nested selectors
  • Easy imports
  • Variables

Today, modern CSS already supports CSS custom properties, also known as CSS variables.

CSS is not a programming language like JavaScript, Python, PHP, Ruby, or Go, where variables are central. CSS is limited and mostly a declarative syntax that tells browsers how to render an HTML page.

But a variable is still a variable: a name that refers to a value. CSS variables help reduce repetition and inconsistency by centralizing value definitions.

They also offer a unique feature CSS preprocessors will never have: you can programmatically read and change a CSS variable value with JavaScript.

Basics of using variables

A CSS variable is defined with a special syntax: add two dashes to the name (--variable-name), then a colon and a value:

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

(more about :root later)

You access the value with var():

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

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

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

Defining variables on any element

CSS variables can be defined on 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 across these examples is scope.

Variable scope

Defining variables on a selector makes them available to all of its descendants.

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

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

:root is a CSS pseudo-class that identifies the document, so a variable on :root is available to every element on the page.

This is similar to targeting the html element, except :root has higher specificity.

If you add a variable to .container, it is only available to descendants of .container:

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

Using it outside that element will not work.

Variables can be overridden:

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

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

Outside .container, primary-color is yellow; inside it is blue.

You can also set or overwrite a variable in HTML with an inline style:

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

CSS variables follow normal CSS cascade rules, with priority based on specificity.

Interacting with CSS variables from JavaScript

The coolest thing about CSS variables is that you can read and edit them with JavaScript.

Set a variable value with plain JavaScript:

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

To read a variable defined on :root:

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

Or, for a variable scoped to a specific element:

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 that does not accept that value, it is treated as invalid.

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

In that case the declaration is invalid and ignored.

Browser support

Browser support for CSS variables is very good, according to Caniuse.

You can use CSS variables today if you do not need Internet Explorer or older browser versions.

If you do need older browsers, libraries like PostCSS or Myth can help — but you lose the ability to interact with variables via JavaScript or browser DevTools, because they compile down to plain CSS without variables (and you lose much of what makes CSS variables powerful).

CSS variables are case-sensitive

This variable:

--width: 100px;

is different from:

--Width: 100px;

Math in CSS variables

To do math with CSS variables, use calc(), for example:

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

Media queries with CSS variables

Nothing special here — CSS variables work normally with media queries:

body {
  --width: 500px;
}

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

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

Setting a default value for var()

var() accepts a second parameter — a fallback when the variable is not set:

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