How to Add a Dark Mode Toggle to a Jekyll Site

| programming

Adding a dark mode switch to a blog made with a static site generator such as Jekyll or Hugo is surprisingly simple. What’s odd is that I haven’t seen any themes that incorporate a toggle. Until that changes, it’s still relatively straightforward to implement a dark mode switch yourself.

My requirements for a dark mode switch

  1. A true switch. Sometimes I prefer light mode, sometimes dark. I didn’t want a theme that was always dark.
  2. Everything has to be client side. This is a static site. There’s no server to run scripts on. Big sites like Slack and Twitter use server side magic to serve up a dark mode.
  3. No cookies or local storage. The EU cookie banner is annoying, and private browsing can break this option.
  4. Consistency. If I’m in dark or light mode, I should never leave it unless I click on the mode switcher.

Dark mode CSS

The first step is to create a dark mode CSS. Some people use JavaScript to add a dark mode class. I prefer to have a completely separate stylesheet. Jekyll’s SCSS support makes this easy.

I have a file with with all of my CSS rules in my _includes folder. I then have separate Sass files for both light and dark modes. Each file contains only variables and and an {% include css.sass %}.

//  Variables for dark mode colors 
 
$main-background-color: hsl(0, 0, 1%); // hsl(0, 0, 99%)
$main-text-color:  hsl(0, 0, 90%); // hsl(0, 0, 10%)
$light-text-color: hsl(0, 0, 70%); // hsl(0, 0, 30%)
$lighter-text-color: hsl(0, 0, 55%); // hsl(0, 0, 45%)
$dark-text-color: hsl(0, 0, 95%); // hsl(0, 0, 5%)
$light-border-color: hsl(0, 0, 20%); // hsl(0, 0, 80%)
$underline-color: hsl(0, 0, 50%); // hsl(0, 0, 60%)
$box-background-color: hsl(0, 0, 10%); // hsl(0, 0, 95%)
$highlight-color: hsl(0,0,40%); // hsl(0, 0, 60%)

// common CSS for both light and dark mode 

{% include css.scss %}

URL parameters to the rescue

The URL parameter ?darkmode triggers the dark mode stylesheet to load instead of the default light mode. A bit of Javascript adds or removes ?darkmode from internal links to make sure readers don’t inadvertently leave their preferred mode.

Add an ID to the stylesheet in the page <head> so you can change the stylesheet with JavaScript:

<link id="CSS" rel='stylesheet' type='text/css' href='/css/style.css'>

Don’t leave the href attribute blank. Otherwise your page will load with no CSS at all if JavaScript is disabled.

This is my mode switch in <nav> menu:

<a href='' id='colorChanger'></a>

The link text empty so that it won’t appear if JavaScript is disabled.

Then I added this script to the end of each page:

<script type="text/javascript">

// looks for the ?darkmode parameter 
// sets global variable darkMode to true or false

const enteredURL = window.location.href;
if (enteredURL.includes('?darkmode')) {
	var darkMode = true; 
	} else {
        var darkMode = false;
    }

// these variables create the URL for switcher in the nav menu 

const darkModeLink = enteredURL + "?darkmode";
const lightModeLink = enteredURL.replace('?darkmode', '');

// loop through links on page to add '?darkmode' to internal links 
	
if (darkMode) {
	document.getElementById('CSS').href="/css/darkmode.css";
	const allLinks = document.getElementsByTagName("a");
	for (let i = 0; i < allLinks.length; i++) {
		if (allLinks[i].href.includes('https://derekkedziora.com/')
			|| allLinks[i].href.includes('http://127.0.0.1:4000/')) {
			allLinks[i].href = allLinks[i].href + '?darkmode';
		}
	}

// it's important for this to come after internal links have '?darkmode' added
// otherwise the switch in the nav menu to turn on light mode won't work!  

	document.getElementById('colorChanger').innerHTML="Light Mode";
	document.getElementById('colorChanger').href=lightModeLink;	
  
// sets nav switcher to allow dark mode to be enabled 

if (darkMode === false) {
	document.getElementById('CSS').href="/css/style.css";
	document.getElementById('colorChanger').innerHTML="Dark Mode";
	document.getElementById('colorChanger').href=darkModeLink;
}
</script>

CSS-only solutions are just out of reach

All of the above code will be moot once CSS media queries catch up with the popularity of dark themes. FireFox and Safari already have. We’re waiting on you, Chrome — yet one more reason not to embrace the Chrome monoculture.

Using local storage

Most of the initial feedback I’ve received has focused on the drawbacks of using URL parameters instead of local storage. C’mon! this is a low traffic personal blog. I can’t think of a better place to experiment.

The benefit of using URL parameters is that if you share a link in dark mode, that’s the version that will open up. If you bookmark dark mode, that’s the only version you’ll see regardless of whether you’re using private browsing or not. I admit this is idiocentric, but that’s how I browse the web.

Flavio Copes has a good tutorial for setting up a theme switcher using local storage if that’s the rout you decide to go. When I tested his solution, I still got black flashes while navigating between light mode pages.

Just a start

I don’t pretend to be a JavaScript ninja. I’m a hobbyist that likes the JAM stack and giving ‘static’ sites a dynamic feel with things random post links.

A bit of tinkering could optimize the load time of the main script: Move everything except the link changing loop to the front of the page.

This is a start. Being able to switch back and forth between light and dark themes gives a site an accessibility and UX boost. I hope this will someday be standard across blogging platforms and site generators.

Update a month on

This works great on both iOS and Mac Safari. As far as I can tell, Safari is designed to not display blank flashes between page loads or show unstyled text before a stylesheet loads.

Unfortunately this isn’t the case with Chrome and Firefox. The split second it takes for the JS to decide which stylesheet to load causes a quick flash. No amount of tinkering could solve this.

Alas, I decided to take down my dark mode toggle. I’d rather have light mode work smoothly than have the whole experience look shoddy. So no dark mode until light preference is supported in CSS by all browsers.