How to make a dark mode (vanilla js)

Created: Dec 16, 2022
Last Modified: Dec 25, 2022

! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
javascript
! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
tutorial

Handrolling a dark mode experience using vanilla javascript

In this tutorial, I’ll demonstrate how I created a dark mode ‘switch’ on this website. This site is generated with Hugo so I get to separate code using it’s built in templating language and Tailwind CSS utility classes. If you’re using neither of these, I’ll explain what the classes are doing so that it’s a generalized solution.

(Go ahead, try it out, it should be to your top right ↗)

Other than that, I use vanilla javascript that can live in the head of your document, which references the users preferences, and stores a variable in the browser’s session storage to remember what mode you were in. The code that goes along with this tutorial can be found at the github repo for this website in layouts/partials/dark-model.html

The concept

Simply, how does this work?

Most simply, you can make a dark mode by attaching a class such as ‘.dark’ to all of the elements that you want to make dark. take the example

.container {
  background: white;
}
.dark .container {
  background: black;
}
<div class="container">
... 
<div class="dark container">

Background

To begin, these techniques are nothing new, so I wanted to give some insight into why I’m even solving this problem! I’ll outline a couple thoughts I had and share the reasoning the ‘why and how’ of everything.

Needs: 🗒️

  • There are also some things that I don’t like about current dark modes!
    • Harsh transitions (a.k.a. no transition at all)
    • What’s happening? Is this site always this dark? How do I change this easily?
  • Persistence across pages
  • Respect for user preferences
  • Smooth, pleasant transitions between modes
    • however, no harsh transitions when switching pages (mode is already selected)

Let’s get into it:

Markup and CSS

Let’s start with the markup: here, I’m using the trick with a hidden checkbox to act as a ‘button’ so you don’t have to keep track of state in a special way: the checkbox has a true or false value attached to it in the DOM.

The following below is a template partial that I wrote called darkmode.html

<label>
  <input type="checkbox" class="hidden" id="toggle" />
  <!-- since the input is hidden, whatever is in the span class below will be the switch / visual aid -->
  <span class="toggle"></span>
</label>

Making a toggle slider with pure CSS is a fairly hacky feeling solution, and. ** some important details above are the <input>

The trick for this is binding the <input> class to .toggle:before class using the + operator, aka the Adjacent sibling combinator in order to

input:checked + .toggle:before {
  transform: translateX(1.5rem);
}
Expand Toggle Button CSS
.toggle {
  /* visual styles */
  background: #bbb;
  border-radius: 3rem;
  /* positioning */
  position: absolute;
  top: 0.5rem;
  right: -3rem;
  /* sizing */
  width: 3.5rem;
  height: 2rem;
  /* transition */
  transition: 400ms;
  /* other */
  cursor: pointer;
}

.toggle:before {
  /* basic styles */
  background: #333;
  border-radius: 3rem;
  /* positioning */
  position: absolute;
  left: 0.2rem;
  bottom: 0.2rem;
  /* sizing */
  height: 1.5rem;
  width: 1.5rem;
  /* transitioning */
  transition: 400ms;
  /* other */
  content: "";
}

The Code

All of the above was just HTML and CSS.. no logic! (except for the operators in CSS)

Firstly, we’ll target our elements that we want affected.

// create constants for DOM elements
const toggle = document.getElementById("toggle");
const html = document.getElementsByTagName("html")[0];

Our setDarkMode function:

Note: it’s important to only attach the ‘transition-class’ to the page when switching between modes. You don’t want the page to have a strong transition each time you switch pages, for example.

/**
 * Returns x raised to the n-th power.
 *
 * @param {boolean} isDark which state you're switching to.
 * @param {boolean} hasTransition Whether this toggling action should have a transition.
 */
 const setDarkMode = (isDark, hasTransition) => {
    if (hasTransition) {
        html.classList.add("transition-class");
    } else {
      html.classList.remove("transition-class");
    }
    if (isDark == true) {
      toggle.checked = true;
      html.classList.add("dark");
      sessionStorage.setItem("isDarkMode", "true");
    } else {
      toggle.checked = false;
      html.classList.remove("dark");
      sessionStorage.setItem("isDarkMode", "false");
    }
  };

Logical flow


/* logic */
if (sessionStorage.getItem("isDarkMode") != null) {
  if (sessionStorage.getItem("isDarkMode") == "true") {
    setDarkMode(true, false);
  } else {
    setDarkMode(false, false);
  }
} else {
  if (window.matchMedia) {
    if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
      setDarkMode(true, true);
    } else {
      setDarkMode(false, true);
    }
  }
}

Mount the Listeners.


/* listeners */

// Listener for a change in checkbox
toggle.addEventListener("change", (e) => {
  e.target.checked ? setDarkMode(true, true) : setDarkMode(false, true);
});

// Listener for a change in Media Query (from system UI)
window.matchMedia("(prefers-color-scheme: dark)").addListener((e) => {
  e.matches ? setDarkMode(true, true) : setDarkMode(false, true);
});

… And that’s it! :) Thanks for reading and I hope this helped.

read more:

This blog uses Hugo"
Next
Nearly Lossless Image Compression with WebP"