photo of lightning in the distance
Photo by Evg Klimov on Unsplash

Dynamic Styling with CSS Variables

Learn the basics of CSS variables, also known as CSS custom properties

5 min read

Sometimes referred to as "custom properties", CSS variables have been available to use for several years now with great browser support.

If you’ve used a preprocessor like SASS or LESS, then you’re familiar with the concept of variables in CSS. But CSS custom properties take that to a whole new level, allowing for more flexibility and truly dynamic CSS!

In this article, you’ll learn what CSS variables are and how you can use them in production today.

This is an intro to the concept of CSS variables, but they’ve been leveraged in other articles on this site like Adding Click Animations to Anything, Building a Segmented Control Component, and How to Build an Expandable Comment Box.

The terms “custom properties” and “variables” are used interchangeably, but they refer to the same thing.

What Are CSS Variables?

Custom properties (sometimes referred to as CSS variables or cascading variables) are entities defined by CSS authors that contain specific values to be reused throughout a document. - MDN Docs

At their very core, CSS variables help reduce repetition in your CSS.

For example, most apps or websites have style properties that are repeated frequently, whether its colors, spacing, or dimensions. Any of these can be good candidates for variables.

First, give them a name prefixed with a double-dash.


:root {
  --black: #111;
  --green: #00ea39;
}

After defining variables, you can use them pretty much anywhere with the var() syntax.


img {
  border: var(--black);
}

.article a {
  color: var(--green);
}

What is this :root selector above? Defining variables here allows you to use them anywhere in your CSS. Think of this as the equivalent of the html element that’s normally targeted with high-level styles applied to the whole page.

Scoping Variables

Depending on where they’re defined, variables can be scoped. This keeps them contained and applied only when necessary.

For example, maybe you have a variable repeated within an element. You can define it on a specific selector.


.interactive-page {
  --transition-speed: 0.3s;
}

.interactive-page .fade {
  transition: opacity var(--transition-speed) ease-in;
}

This keeps the variable contained to the interactive-page class. A variable with the same name can be scoped to a different selector and hold a different value. Neither of them will collide or be overridden, so long as one variable isn’t the child of another.


.about-page {
  --transition-speed: 1s;
}

Scoping variables this way allows you to be more generic with your naming, with one exception - variables can override others declared higher up in the cascade.

For example, a color declared on the body with a name of --color can be redeclared in the footer and that color will take precedence. The footer color will be blue.


body {
  --color: orange;
}

footer {
  --color: blue;
}

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


Using scoped variables, along with CSS nesting, can allow for “components” in your CSS, so long as you’re careful.

Getting and Setting Variables in Javascript

Now let’s talk about the best part of CSS variables: retrieving and updating them dynamically with Javascript!

Let’s assume we have the following CSS.


.hero-section {
  --background: #fff;

  background: var(--background);
}

A property can be set using the style.setProperty() method.


const section = document.querySelector('.hero-section');
section.style.setProperty('--background', '#000');

This will update the --background variable from white to black.

Getting the value of a variable can be done by first getting the computed styles via getComputedStyle, then looking for a specific name using getPropertyValue.


const rootStyles = getComputedStyle(document.documentElement);
const color = rootStyles.getPropertyValue('--background');

The above returns the value of what --background is currently set to.

Using document.documentElement will get the styles set on the :root , but you may need to be more specific depending on where the variable was declared. For example, if you have a variable declared on a class of .container, then you’ll need to select that element in the DOM and pass it into getComputedStyle.

An obvious use case for setting variables dynamically like this is to implement dark mode. But the only limit is your imagination, really. Any scenario where several values need to be updated all at once is a good candidate for CSS variables.

Dark Mode Example

Because we mentioned dark mode in the previous section, let’s implement a basic variation of one. We’ll update the background and text color using a click event.

First, let’s add the HTML for the buttons.


<div class="darkmode">
  <button id="light">Light
  <button id="dark">Dark
<div>

Now we have a few CSS variables to define. --current-color and --current-background are dynamic variables that we'll update in a minute.


:root {
  --black: #111;
  --white: #fff;

  --current-color: var(--black);
  --current-background: var(--white);
}

These variables are then applied to the color and background of the page.


body {
  font-family: Arial, Helvetica, sans-serif;
  color: var(--current-color);
  background: var(--current-background);
  transition: background 0.5s ease, color 0.5s ease;
  line-height: 1.4;
}

The transition property above is for a subtle fade effect when toggling between light and dark.

Next, we have some Javascript.

First, get the light and dark trigger buttons.


const lightBtn = document.getElementById('light');
const darkBtn = document.getElementById('dark');

Now let’s write a utility function to consolidate updating the variables.

The function takes element, variable and value as parameters and updates a variable to a new value. This prevents needing to write something like style.setProperty('variable', 'value') several times.


const setCSSVariable = (element, variable, value) => {
  element.style.setProperty(variable, value);
}

Next, define a function for when the “light” button is clicked.


const lightBtnClick = () => {
  setCSSVariable(document.documentElement, "--current-color", "#111");
  setCSSVariable(document.documentElement, "--current-background", "#fff");
}

The “dark” button function does the exact opposite.


const darkBtnClick = () => {
  setCSSVariable(document.documentElement, "--current-color", "#fff");
  setCSSVariable(document.documentElement, "--current-background", "#111");
}

Finally, add some event listeners for each of the buttons.


lightBtn.addEventListener('click', lightBtnClick);
darkBtn.addEventListener('click', darkBtnClick);

If you’d like to expand on what you learned in this section, be sure to check out Building a Dark Mode Theme Toggle.

Downsides

Now that we’ve seen what CSS variables can do, are there any downsides to consider?

At the time of this writing, you can’t use variables inside of media queries.


:root {
  --min-width: 320px;
}

@media screen and (min-width: var(--min-width)) {}

The above won’t work. A great explanation about why can be found here.

Outside of this, I haven’t run into any other issues using CSS variables in production. Unless you still need to support IE11 for some reason, the browser support has been great for a while!

Summary

CSS variables, also known as CSS custom properties, offer more flexibility and dynamic styling in CSS. They help reduce repetition by allowing you to define reusable values throughout your CSS. Variables can be scoped to specific selectors and can be easily updated and retrieved dynamically using JavaScript. While there are some limitations, such as not being able to use variables inside media queries, CSS variables have great browser support and are a powerful tool for creating more maintainable and dynamic styles.