Notes to self

Implementing dark mode with Tailwind and Stimulus

About to implement a dark mode into your Rails app? Here’s one way using Tailwind and Stimulus.

Dark mode

Dark mode can be automatic based on system settings or manual based on user action.

The preference for dark mode will be based on the prefers-color-scheme: dark media selector and a custom settings in localStorage. If you don’t need a custom switch you could implement this all just with the media selector and Tailwind.

The dark theme itself will work based on adding a dark CSS class to the html element. We’ll then configure Tailwind to use this selector to drive the theme change.

Stimulus

Stimulus controller needs to support a toggle action for manual switching and a way to get saved settings from a localStorage:

// app/javascript/controllers/dark_mode_controller.js

import { Controller } from "@hotwired/stimulus";

 export default class extends Controller {
   toggle() {
     const isCurrentlyDark = document.documentElement.classList.contains("dark");
     this.applyDarkMode(!isCurrentlyDark);
     this.setUserPreference(!isCurrentlyDark);
   }

   applyDarkMode(isDark) {
     if (isDark) {
       document.documentElement.classList.add("dark");
     } else {
       document.documentElement.classList.remove("dark");
     }
   }

   getUserPreference() {
     return localStorage.getItem("dark-mode");
   }

   setUserPreference(isDark) {
     localStorage.setItem("dark-mode", isDark ? "true" : "false");
   }
 } 

You could also add an action for clearing this settings.

We then registrer the controller as usual. For example:

// app/javascript/controllers/index.js

import DarkModeController from "./dark_mode_controller"
application.register("dark-mode", DarkModeController)

The switch

To support manual switching, we need to initialize the above Stimulus controller somewhere on the page:

<div data-controller="dark-mode">
  <a data-action="dark-mode#toggle" class="p-2 mt-4">💡</a>
</div>

You can use a simple bulb emoji as I use on Tube and Chill.

Tailwind

Now that we are good to go to design the dark theme, we’ll instruct Tailwind to build darkMode with the class (before v3.4.1) or selector strategy:

// tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: "selector",
  content: [
    './app/views/**/*.html.erb',
    './app/helpers/**/*.rb',
  ]
}
...

Note that you could also use the media strategy if you don’t intent to build a custom switch.

Once that’s done, you can start adding dark:* and dark:hover:* classes to elements:

<div class="text-black bg-white dark:text-white dark:bg-slate-800">
  Dark background in dark mode.
</div>

Logotypes

If we use src with images and don’t want to switch to background images, we can include both elements on the page:

<img class="block dark:hidden" src="/images/<%= image_name %>.png" />
<img class="hidden dark:block" src="/images/<%= image_name %>_dark.png" />

Autoload

To have the right style load immediately we’ll add a short JavaScript in the head of the page:

<script>
if (localStorage.getItem("dark-mode") === "true" || (!("dark-mode" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
 document.documentElement.classList.add("dark")
}
</script>

This handles a previous saved state or browser settings.

And that’s it!

by Josef Strzibny
RSS