Showcasing a simple dark mode implementation
Today, let's look at implementing the simplified light and dark mode toggle I shipped for Showcase.
Showcase lets you build previews for your partials, components, view helpers, Stimulus controllers and more. It's a more Rails Native feeling alternative to Lookbook, since it's just a Rails engine and there's no JavaScript to setup.
You can see Showcase in action here, with the light and dark mode.
Let's dive in.
First Steps from Dr. Nic
I started off with Dr Nic's Dark Mode for Jumpstart post and it made me realize that Dark Mode toggle would be a lot easier than I'd thought.
I recommend reading the post in full before continuing.
Already, there's a number of things going through my mind:
- I'm shipping a Rails engine with Showcase, so I can't assume Stimulus
- We're toggling the
hidden
class in JavaScript, could we use CSS for that?
Then, reading Tailwind's Dark Mode documentation, they're also mentioning being careful about Flash of Unstyled Content (FOUC). They're specifically saying to put the dark mode switching in the head
tag directly to prevent FOUC.
But then, if we wait on Stimulus to load first, we could also increase the chance of FOUC.
If we simplify the implementation, maybe it's easier than a whole Stimulus controller
Slimming the implementation
To keep maintenance easy, I knew I wanted UI that was clear and simple. This should just work and hopefully not cause users to open issues on GitHub.
Thinking of maintainability like this and reducing the cost of a feature, is part of being an Open Source contributor
Cutting reset to system setting
Immediately, the usual "reset to system setting" that dark mode implementations have could go. Showcase isn't visited that much and having to click light mode
or dark mode
once after having changed your system mode didn't seem like the biggest deal. Score!
Hiding and showing toggles via CSS
classList.add("hidden")
— whenever I see JavaScript like this, that toggles the hidden
class on one of more elements, my instincts kick in and there must be a more modern CSS way to do the same.
Since Tailwind's dark mode, in this case, works by assigning <html class="dark">
that's state we can reach with CSS.
So we can have our dark mode toggle shown in light mode but hidden in dark mode with class="block dark:hidden"
.
And then for the light mode toggle, the opposite, with class="hidden dark:block"
.
Extracting a containing class in head
To help prevent the Flash of Unstyled Content (FOUC) problem that Tailwind highlighted, we'll throw our code into head
to have it run immediately.
However, to ease maintenance, we must find an abstraction that keeps the code encapsulated and simple.
Here's what I ended up with:
<head>
<title>Showcase</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>
// We're setting this directly here to help prevent Flash of Unstyled Content (FOUC).
class Showcase {
static start() {
// Check the system setting on first load and use it as the default
const preference = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
// On secondary loads localStorage will have a setting, that we'll use instead
this.colorScheme = localStorage.colorScheme || preference
}
static set colorScheme(value) {
// Stash the value in localStorage, so the mode persists after reloads
localStorage.colorScheme = value
// Then set the class on `<html>`, using the underrated `toggle` function
document.documentElement.classList.toggle("sc-dark", value === "dark")
}
}
Showcase.start() // "start" our helper class immediately to assign the colorScheme as early as possible
</script>
<%= render "showcase/engine/head" %>
<%= render "showcase/engine/stylesheets" %>
<%= render "showcase/engine/javascripts" %>
</head>
Notice we're setting up the script
tag before loading our other CSS and JavaScript assets to further prevent FOUC.
classList.toggle
is extremely useful and still relatively unknown. If you're doingadd
andremove
wrapped inif
s, consider replacing them with a singletoggle
call.
Wiring up the light and dark mode toggles
With our Showcase
helper class, we can now call Showcase.colorScheme = "dark"
to enable dark mode, and pass "light"
for light mode.
We can use the onclick
event on anchor tags to call these functions, like this:
<a onclick="Showcase.colorScheme = 'light'" class="sc-hidden dark:sc-block">light mode</a>
<a onclick="Showcase.colorScheme = 'dark'" class="sc-block dark:sc-hidden">dark mode</a>
(sc-
is our configured Tailwind prefix so we don't clash with application styles).
Here's the source lines in Showcase.
Tweaking to more conventional JavaScript
It's been a while since I've last read this code, and I seeing it now I'd make this change:
class Showcase {
constructor() {
const preference = window.matchMedia()
// …
}
set colorScheme(value) {
// …
}
}
window.showcase = new Showcase()
And then on the toggles we can do showcase.colorScheme = "dark"
.
A tiny change, but it feels more like we're following JavaScript conventions.
Summary & Next Steps
We looked at the implementation of a light and dark mode toggle.
We loaned a community members blog post, and thus stood on the shoulders of giants, to help us see new ways to simplify our implementation.
We then implemented those simplifications and wound up with code that's more suited for our specific purpose — and easier for us to maintain in our gem.
Feel free to copy and adapt this code to your app's dark mode.
If you've liked this post or the ones in the archive, please share this newsletter with your coworkers or on social media.
Thanks for reading!