Ever since I last redesigned this site, I intended to add a dark mode to it. I even had a design semi-ready under the hood but then never really got around to finally implement it.
A while ago, I stumbled upon this post by Richard Rutter and he touched on a lot of the things and goals I had in mind as well, so I decided to take it as a starting point and finally get to it again.
Overall, it’s heavily inspired by the techniques outlined by Richard Rutter and some of the posts he mentioned, but I did change a few things here and there which I want to document here.
Basic functionality
Here’s my list of goals that I initially put together:
Respect OS/Browser default: When a page is loaded, the system or browser default should be respected and the correct color theme should be rendered.
Accessible Toggle: There should be an accessible toggle to switch the theme. If light mode is active it should switch to dark and vice versa.
Save selection to make it persistent: Once set, the theme value should be saved somehow, to make sure it’s persistent through subsequent page reloads. When the page is loaded, this value should be used to render the correct theme. This manual setting should take precedence over the OS/browser default. I will use cookies instead of localStorage for this part.
Progressive Enhancement: JavaScript will be used to enhance the experience, namely by toggling the theme without reloading the whole page. If JavaScript fails, is inactive or is not loaded for any other reason, I want everything to degrade gracefully, but would like the base functionality to still work without JS.
What do we need to make this happen?
- HTML markup for the toggle
- A server side function (PHP in my case) to determine which theme to render, depending on OS/browser settings or the user’s manual selection.
- A JavaScript function to enhance this experience by intercepting clicks on our toggles and do the switching without a full page reload
With these goals in mind, let’s jump straight in.
The Markup of the toggles
There are various options to add our toggles. Plain link elements might have worked but didn’t feel right, as the interaction is not so much about navigating somewhere, but more a change of state. In the post mentioned above, Richard went with radio buttons, because they seemed most appropriate for exclusive options like these. This makes a lot of sense, but radio buttons do need a little bit of JavaScript to directly trigger an interaction and I wanted to see if I could get the basic functionality to work without any JS at all.
I ended up using <button>
elements with a type="submit"
, placed inside a <form>
, which makes it possible to trigger the interaction without any JS at all and semantically still seemed correct enough to me.
When one of the buttons is clicked, the form gets submitted and sends a GET request including all the information of the form. An empty action parameter on the form makes it reload the current page we’re on.
The button elements have a name="theme"
and value="dark"
(or light), which will be submitted with the GET request.
Inside the button we’ve got an inlined SVG icon (inlined so we can style it using CSS and are able to use currentColor etc.) and a span with some accompanying text which is visually hidden but important for screen-readers (I believe).
In the example, you can also see the onclick="switchDark()"
(or switchLight) functions added to the toggles, which we’ll come to in a second.
<form method="get" action="" class="theme-toggles">
<button type="submit" name="theme" value="dark" class="theme-toggle" onclick="switchDark()">
<svg fill="none" color="currentColor" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path xmlns="http://www.w3.org/2000/svg" d="..." fill="currentColor"></path>
</svg>
<span class="theme-toggle__text">Dark</span>
</button>
<button type="submit" name="theme" value="light" class="theme-toggle" onclick="switchLight()">
<svg fill="none" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path xmlns="http://www.w3.org/2000/svg" d="..." fill="currentColor"></path>
</svg>
<span class="theme-toggle__text">Light</span>
</button>
</form>
Code language: HTML, XML (xml)
The function to display the correct theme
In the functions.php of my WordPress theme, I added a function that is hooked to the filter template_redirect
, which is when the template is being loaded. In there I check for our GET request and cookie.
If one of them is set, I use the body_class
filter to add a class “theme-light” or “theme-dark” to the body element.
First I check if a theme is set in the GET request, indicating that the button was pressed, and if so I set a cookie to save the selection. Otherwise I check if the cookie exists and take this to set the theme, and if none of them is set we fall back to the default values.
The CSS
To switch the color variables of the theme, I added these rules to my CSS. I could probably define those in my theme.json file as well, but for now doing it directly in CSS felt easier.
First, I want to use the correct colors depending on the OS setting, this is done through a prefers-color-scheme
media query.
Then I want to overrule those colors, if a class is explicitly set on the body element.
.theme-dark {
--wp--preset--color--background: #102542;
--wp--preset--color--background-elevated: #152C4B;
--wp--preset--color--highlight: #FF9A3E;
--wp--preset--color--foreground: #A3BCDD;
}
.theme-light {
--wp--preset--color--background: #E8F5F9;
--wp--preset--color--background-elevated: #EFFBFF;
--wp--preset--color--highlight: #F75F0F;
--wp--preset--color--foreground: #163867;
}
@media ( prefers-color-scheme: dark ) {
body {
--wp--preset--color--background: #102542;
--wp--preset--color--background-elevated: #152C4B;
--wp--preset--color--highlight: #FF9A3E;
--wp--preset--color--foreground: #A3BCDD;
}
}
Code language: CSS (css)
The JavaScript
Up until this point, everything worked perfectly fine without any need for JavaScript at all. The switch is handled by standard/native form events and rendered on the server, adding classes to the markup which then get styled using CSS. This visibly adds a ?theme=dark
parameter to the URL and reloads the page when we switch themes, which is perfectly fine for a base experience, but can be enhanced a lot by a little bit of JS.
Inspired heavily by Richard’s solution, I added the following bits of JavaScript. The only things I changed is that I use cookies instead of localStorage to save the selected theme. I do this because I want to access/change them on the server as well, which localStorage can’t but cookies can. And because I used submit instead of radio buttons, I added e.preventDefault
to prevent the form from being sent each time it’s clicked.
Here’s my function to switch to the light mode:
function switchLight(e) {
e.preventDefault();
document.body.classList.remove("theme-dark");
document.body.classList.add("theme-light");
document.cookie = "theme=light; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT";
}
Code language: JavaScript (javascript)
This toggles the classes on the body and then sets a cookie with an expiration date way into the future – I should probably not hardcode this value, while I think of it, but you get the point.
Wrapping up
Ok, that’s basically it. The toggle does what it is supposed to do and JS is used to progressively enhance the experience.
I tried to add everything as accessible as I can, but if you are experienced with accessibility and think I missed something or you would do something different for good reasons, please reach out! I would love to learn more.