$ cat use-this-stimulus-controller-for-rails-tailwind-dark-mode.md

rails + tailwind dark mode Stimulus Controller that persists across page loAds

December 19, 2024

Recently I had reason to add dark mode to a Ruby on Rails 8 app. After a few iterations, I landed on the following implementation which I thought I would share. This implementation persists the dark mode setting across page loads using local storage, so it is pretty ideal for most applications.

I am making the assumption here that you are already a Rails developer and don’t need a lot of this explained to you.

Use this button somewhere on a page that is loaded on every page visit (like a navbar or the footer):

<button data-controller="dark_mode" class="flex flex-col justify-center ml-3">
  <div data-dark-mode-target="toggleButton">
    <label class="relative cursor-pointer p-2" for="light-switch">
        <svg class="dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
            <path class="fill-yellow-300" d="M7 0h2v2H7zM12.88 1.637l1.414 1.415-1.415 1.413-1.413-1.414zM14 7h2v2h-2zM12.95 14.433l-1.414-1.413 1.413-1.415 1.415 1.414zM7 14h2v2H7zM2.98 14.364l-1.413-1.415 1.414-1.414 1.414 1.415zM0 7h2v2H0zM3.05 1.706 4.463 3.12 3.05 4.535 1.636 3.12z" />
            <path class="fill-yellow-300" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
        </svg>
        <svg class="hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
            <path class="fill-yellow-300" d="M6.2 1C3.2 1.8 1 4.6 1 7.9 1 11.8 4.2 15 8.1 15c3.3 0 6-2.2 6.9-5.2C9.7 11.2 4.8 6.3 6.2 1Z" />
            <path class="fill-yellow-300" d="M12.5 5a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 5Z" />
        </svg>
        <span class="sr-only">Switch to light / dark version</span>
    </label>
  </div>
</button>

Create a Stimulus controller, name it ‘dark_mode_controller.js’, and add this to it:

import { Controller } from "@hotwired/stimulus";export default class extends Controller {  static targets = &#91;"toggleButton"];  connect() {    this.initializeDarkMode();    this.toggleButtonTarget.addEventListener('click', () => this.toggleDarkMode());  }  disconnect() {    this.toggleButtonTarget.removeEventListener('click', () => this.toggleDarkMode());  }  initializeDarkMode() {    const htmlElement = document.documentElement;    const darkModePreference = localStorage.getItem('darkMode');        if (darkModePreference === 'enabled') {      htmlElement.classList.add('dark');    } else {      htmlElement.classList.remove('dark');    }        this.updateToggleButtonState();  }  toggleDarkMode() {    const htmlElement = document.documentElement;    const isDarkMode = htmlElement.classList.toggle('dark');       localStorage.setItem('darkMode', isDarkMode ? 'enabled' : 'disabled');        this.updateToggleButtonState();  }  updateToggleButtonState() {    const isDarkMode = document.documentElement.classList.contains('dark');        this.toggleButtonTarget.setAttribute('aria-pressed', isDarkMode);    this.toggleButtonTarget.setAttribute('aria-label',       isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'    );  }}

Reload your page and you should have a dark mode stimulus controller that persists the park mode setting across page loads using local storage.

  • Finder
    Finder
  • Jesse Waites
    Jesse Waites
  • Xcode
    Siri
  • Simulator
    Simulator
  • Testflight
    Testflight
  • SF Symbols
    SF Symbols
  • Icon Composer
    Icon Composer
  • Sketch
    Sketch
  • VS Code
    VS Code
  • Postgres
    Postgres
  • Android Studio
    Android Studio
  • Trash
    Trash