Chris D. MacRae

1 days ago · 2 min read · Web Development

How To Do Dark Mode With Static Pages

When you don't have server rendered pages that let you send down a page ready for the user's selected color scheme, it might seem hard. Believe me, it's not!

This website has a light and dark mode, and responds to the user’s preferred color scheme out of the box.

Getting this to work with my fully static website proved to be a real chore, and ended in a ridiculously simple solution.

So for this tutorial, I’ll walk you through setting up a simple light and dark theme, with a button to toggle in on and off.

What we need to do

We need to do a few things:

  1. We need to get the user’s operating system (OS) preferences, if any

  2. We need to apply the users preference to our site

  3. We need a button to allow the user to toggle between light and dark, overriding their OS preference

So, let’s dive right in…

Getting the OS preference

To get the OS preference, we’ll use the window.matchMedia API:

<script>
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
</script>

Now, let’s move on to our button…

Applying the OS preference

Now that we have the users preference, we can use it to apply our theme to the page. We’ll do that by adding a dark class if the theme is dark, and having our light theme be the default.

<script>
  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches);
  const root = document.documentElement;

  if (prefersDark) {
    root.classList.add("dark");
  } else {
    root.classList.remove("dark");
  }
</script>

Now, we need to add styles for our light and dark themes. For now, we’ll do something very simple:

  • If the theme is light, the background should be white

  • If the theme is dark, the background should be black

<style>
  html {
    background: white;
  }

  html.dark {
    background: black;
  }
</style>

This gives us the basic functionality of matching the users operating system preferences. But, what if they don’t want that!?!?

Toggling between light and dark

We’ll create a very simple (and ugly) button to do the toggle.

<button>Toggle theme</button>

Right now, our button doesn’t do anything. Let’s fix that by adding an onclick event:

<script>
  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches);
  const root = document.documentElement;

  if (prefersDark) {
    root.classList.add("dark");
  } else {
    root.classList.remove("dark");
  }

  function toggleTheme() {
    const root = document.documentElement;
    const isDark = root.classList && root.classList.contains("dark");
    if (isDark) {
      root.classList.remove("dark");
    } else {
      root.classList.add("dark");
    }
  }
</script>

<button onclick='toggleTheme()'>test</button>

Finally, we’ll use localStorage so we can persist the override between page views, and use the override if it’s present.

<script>
  const override = window.localStorage.getItem("theme");
  const prefersDark =
    override === "dark" ||
    (!override &&
      window.matchMedia("(prefers-color-scheme: dark)").matches);
  const root = document.documentElement;

  if (prefersDark) {
    root.classList.add("dark");
  } else {
    root.classList.remove("dark");
  }

  function toggleTheme() {
    const root = document.documentElement;
    const isDark = root.classList && root.classList.contains("dark");
    if (isDark) {
      root.classList.remove("dark");
      window.localStorage.setItem("theme", "light");
    } else {
      root.classList.add("dark");
      window.localStorage.setItem("theme", "dark");
    }
  }
</script>

Finally, we must put the script tag in the head element of the page. This ensures that it’s loaded and executed before any of the styles load. This prevents a flash of light colors for users that are using dark mode.

Putting it all together, we get something that looks like:

<html lang="en">
  <head>
    <style>
      html {
        background: white;
      }

      html.dark {
        background: black;
      }
    </style>
    <script>
      const override = window.localStorage.getItem("theme");
      const prefersDark =
        override === "dark" ||
        (!override &&
          window.matchMedia("(prefers-color-scheme: dark)").matches);
      const root = document.documentElement;

      if (prefersDark) {
        root.classList.add("dark");
      } else {
        root.classList.remove("dark");
      }

      function toggleTheme() {
        const root = document.documentElement;
        const isDark = root.classList && root.classList.contains("dark");
        if (isDark) {
          root.classList.remove("dark");
          window.localStorage.setItem("theme", "light");
        } else {
          root.classList.add("dark");
          window.localStorage.setItem("theme", "dark");
        }
      }
    </script>
  </head>
  <body>
    <button onclick="toggleTheme()">Toggle theme</button>
  </body>
</html>

Wrapping up

This gives you a basic example of an API that will work for statically rendered pages.

It’s not perfect, but it gets the job done.