DEV Community

Cover image for Building a dark-mode theme with CSS variables
Pedro F Marquez
Pedro F Marquez

Posted on

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
:root {
  --h1-color: gold;
}

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

h2 {
  color: var(--h1-color);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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");
  });
Enter fullscreen mode Exit fullscreen 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
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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.

Originally posted to https://pedromarquez.dev/blog/2022/7/dark-mode-css

Discussion (0)