kaspth

Subscribe
Archives
July 22, 2024

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 doing add and remove wrapped in ifs, consider replacing them with a single toggle 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!

Don't miss what's next. Subscribe to kaspth:
GitHub X
This email brought to you by Buttondown, the easiest way to start and grow your newsletter.