My latest book "Backend Developer in 30 Days" is out! You can get it at Amazon
← Back to all posts

Building a dark-mode theme with CSS variables

In this post, we will explore CSS variables and the prefers-color-scheme media query, and we will build a small example of how to implement dark-mode in a website without using any external libraries and with little to no use of JavaScript.

Overview

In recent years, “dark mode” has become widely popular. Ever since the beginning of the Internet, web developers have looked for ways to customize their users' experiences with different color palettes; most of the time leading to custom solutions that relied heavily on JavaScript and working around CSS’s rules.

Modern CSS, however, now includes features that allow developers to override easily override styles across a website or web application.

CSS Variables

With many years in the making, browser support for CSS variables expanded around 2016. Now, newer versions of most modern browsers support them.

It’s important to notice that these variables are supported by vanilla CSS; there is no need to use a pre-processor like Sass or Less.

As their name indicates, CSS variables allow us to store values once and use them across our stylesheet.

The following example creates two variables in a body block and one in a h1: body-color, font-color and h1-color.

body {
  --body-color: black;
  --font-color: white;
  background-color: var(--body-color);
  color: var(--font-color);
}

h1 {
  --h1-color: gold;
  color: var(--h1-color);
}

Notice that variables are prefixed with --. That is how CSS recognizes that they are variables.

Then, within those same blocks, we apply CSS rules like background-color and color, and we get the variables’ values by passing them to the function var().

While this example may seem over-simplistic, you can see the potential: We can take those variables and put them in a global scope:

:root {
  --body-color: black;
  --font-color: white;
  --h1-color: gold;
}

body {
  background-color: var(--body-color);
  color: var(--font-color);
}

h1 {
  color: var(--h1-color);
}

Notice that we moved the variable definitions to a block named :root. This pseudo-class matches the <html> tag. This means that, from here, all CSS rules on the page will have access to these variables.

A web page with a black background, gold-colored title and white font text

Some benefits

CSS variables allow us to centralize and standardize values for CSS rules across our web page. You can encapsulate specific values in reusable variables.

Most websites use a palette of 3 or 4 colors: primary, secondary, and accent colors. We can create a variable for each, and refer to those variables directly in every other CSS rule. If you want to change the palette, just change the variable definitions; no need to update each rule in every stylesheet.

Overriding CSS variables.

CSS variables follow the same rules as the rest of CSS; thus, variables can be overridden:

<h2>This is gold</h2>
<h2 class="page1">This is green</h2>
:root {
  --h1-color: gold;
}

h2.page1 {
  --h1-color: green;
}

h2 {
  color: var(--h1-color);
}

Since we can override CSS rules by adding specificity to the selector, we can also override the variable definitions in :root:

:root {
  --body-color: white;
  --font-color: black;
  --h1-color: gold;
}

// we only override the variables we need:
[data-dark-mode] {
  --body-color: black;
  --font-color: white;
}

Now the :root block contains the variable definitions for “light-mode”, and the block [data-dark-mode] overrides the body-color and font-color variables.

This means that, if the element <html> has the attribute data-dark-mode, the page will have a black background and white font, instead of the default white background and black font:

<!-- Light mode: -->
<html>
  <!-- ... -->

  <!-- Dark mode: -->
  <html data-dark-mode></html>
</html>

The attribute data-dark-mode is no reserved word. This attribute can be anything like data-dark-theme or data-cool-dark-mode-on.

Just remember to prefix the attribute with data-, as this is a custom attribute.

Switching between dark and light modes

If we want to allow users to switch between light and dark modes, we will need a couple of JavaScript lines. First, create a button:

<button id="dark-mode-toggle">Toggle dark mode</button>

Then, add an event listener for the “click” event of that button. Inside that event listener, get a reference to the html element, and use the toggleAttribute method:

document
  .querySelector("#dark-mode-toggle")
  .addEventListener("click", function () {
    document.querySelector("html").toggleAttribute("data-dark-mode");
  });

And that’s it! When the user clicks the button, the attribute data-dark-mode will be toggled on and off the <html> element, overriding the styles.

The “prefers-color-scheme” media query

Users now can configure their operative systems (Android, iOS, MacOS, and so on) to use dark-mode everywhere, when possible. Browsers can detect this setting through media queries, specifically, the prefers-color-scheme media query:

@media (prefers-color-scheme: light) {
  // detect OS-configured light-mode and apply rules
}

@media (prefers-color-scheme: dark) {
  // detect OS-configured dark-mode and apply rules
}

Using these media queries, we can set the page to always be in dark-mode if the user configured this option in their device:

:root {
  --body-color: white;
  --font-color: black;
  --h1-color: gold;
}

[data-dark-mode] {
  --body-color: black;
  --font-color: white;
}

@media (prefers-color-scheme: dark) {
  :root {
    --body-color: black;
    --font-color: white;
  }
}

Optionally, we can remove the dark-mode toggle button if the option is already set at the OS level:

// ...
@media (prefers-color-scheme: dark) {
  :root {
    --body-color: black;
    --font-color: white;
  }
  #dark-mode-toggle {
    display: none;
  }
}

The best part of this media query is that it removes the need of using JavaScript to apply dark-mode to the page. But, as always, make sure that the browsers your expect to target support the media query.

An easy way to test prefers-color-scheme is through Chrome's Rendering tools:

Chrome's rendering tools showing a dropdown to emulate prefers-color-scheme

Codepen

See the Pen CSS variables and dark-mode by Pedro Marquez (@pfernandom) on CodePen.

Conclusion

CSS variables are extremely useful to standardize our stylesheets and make them configurable.

In this post we implemented dark-mode in a very simple web page, but the benefits of CSS variables and the prefers-color-scheme media query are more apparent once we apply them in a real application with tons of CSS rules.