Adding Dark Mode to a Static Site

The internet is finally viewing the ability to switch back and forth between a dark theme or night mode and a light mode as an important accessibility feature. It’s about time. Staring at an overly illuminated screen all day is hard on the eyes, and trying to reduce evening light exposure has worked wonders for my sleep quality.

If you have a static site—my blog is built with Jekyll, but this is same concept if you use Hugo or Gatsby, you can’t use the server to change your CSS from the backend to switch back and forth between light and dark modes.

There’s three ways to change CSS on the front end so that whoever is reading a site will still in the color mode they selected:

  1. Cookies. A no go for me because the EU cookie thing is annoying.
  2. URL Parameters. I got this to work, but URLs are clunky looking and this sometimes breaks navigation to different parts of the same page.
  3. Local Storage. The clear winner: unnoticeable to readers and works well with the new prefers-color-scheme in CSS.

The Concept

To be clear I wanted a switch that allows readers to select whichever color mode they want, and that mode needs to not revert to the default when someone navigates around the site. Furthermore, I didn’t want a flash of either black or white when moving between pages.

This meant that I needed a button that triggered a script to change the page’s CSS and add local storage telling future page loads which color scheme to use.

To make it even more complicated, I wanted the default to be day mode unless someone’s system preferences were set to dark mode. Thus I’d need some frontend JavaScript to figure out what the color scheme of the initial page load and save the preference to local storage.

The CSS

On my first attempt, I used two CSS files and swapped them out based on the preferred color scheme. This caused flashes of white or black when moving between pages in Firefox and Chrome. This is a common shortcoming of a lot of sites with dark mode toggles.

The solution is simple enough: use a single CSS file with variables. Here’s what my CSS looks like:

html, html[data-theme="light"], {
  --light-text-color: hsl(0, 0%, 35%);
  --main-background-color: hsl(0, 5%, 95%);
  --main-text-color: hsl(0, 0%, 0%);
}

html[data-theme="dark"] {
  --light-text-color: hsl(0, 0%, 60%);
  --main-background-color: hsl(0, 0%, 10%);
  --main-text-color: hsl(0, 10%, 80%);
}

By default, the light mode variables will be set and that’s how the site will load. To get dark mode to load, we have to use JavaScript to change HTML data attribute to “dark”. Going back to light mode is same in reverse, changing the HTML data attribute to “light”.

Of course, when you specify the colors for various elements in your CSS use the variables and not absolute values!

The last bit of CSS is adding a way to detect whether a site visiter prefers dark mode:

@media (prefers-color-scheme: dark) {
    html {
      content: "dark";
    }
}

The button

I put a simple button at the top of each page on my site:

<button id="theme-toggle" onclick="modeSwitcher()"></button>

The JavaScript

At the very top of the page, I inserted a tiny script to check if dark mode has already been entered into local storage. If you run this at the end of the page, you’ll get a white flash when navigating between posts.

const theme = localStorage.getItem('theme');
	if (theme === "dark") {
		document.documentElement.setAttribute('data-theme', 'dark');
	}

At the end of each page I inserted this script:

const userPrefers = getComputedStyle(document.documentElement).getPropertyValue('content');	

if (theme === "dark") {
	document.getElementById("theme-toggle").innerHTML = "Light Mode";
} else if (theme === "light") {
	document.getElementById("theme-toggle").innerHTML = "Dark Mode";
} else if  (userPrefers === "dark") {
	document.documentElement.setAttribute('data-theme', 'dark');
	window.localStorage.setItem('theme', 'dark');
	document.getElementById("theme-toggle").innerHTML = "Light Mode";
} else {
	document.documentElement.setAttribute('data-theme', 'light');
	window.localStorage.setItem('theme', 'light');
	document.getElementById("theme-toggle").innerHTML = "Dark Mode";
}

function modeSwitcher() {
	let currentMode = document.documentElement.getAttribute('data-theme');
	if (currentMode === "dark") {
		document.documentElement.setAttribute('data-theme', 'light');
		window.localStorage.setItem('theme', 'light');
		document.getElementById("theme-toggle").innerHTML = "Dark Mode";
	} else {
		document.documentElement.setAttribute('data-theme', 'dark');
		window.localStorage.setItem('theme', 'dark');
		document.getElementById("theme-toggle").innerHTML = "Light Mode";
	}
}

This script checks if a visitor prefers dark mode, and if so applies dark mode and notes it in local storage. If someone clicks the button, the mode gets changed and local storage updated accordingly. Additionally, the text of the button is updated to match the current color mode.

The result

Like magic, you now have a dark mode toggle that works on a static site. You can have fun with this and add toggles for other things like sans-serif or serif fonts.