Rails and Tailwind dark mode switch walk through

January 17, 2024

This is quick step by step guide to creating a dark mode switch for Rails with Tailwind using Stimulus.

Before we start, I like to use Font Awesome for icons - which I will do in this walk through. You can absolutely skip this part and then modify to use your own images, SVGs, etc.

But here we go.

Add the Font Awesome gem to your Gemfile

#Gemfile
gem 'font-awesome-sass', '~> 6.5', '>= 6.5.1'

bundle install

Now go into your stylsheets folder and change your application css to an scss file. Simply rename it to application.css.scss.

Then import Font Awesome, like below.

 /*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */

 @import "font-awesome";

Next you need to tell your Tailwind config file to allow the preference to be set by the user.

Go to config/tailwind.config.js and add the export that tells the config we will manually control dark mode by the class method when dark is preferred by the user - which will be controlled by the switch we will buiild.

// tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
  darkMode: 'class',
  content: [
    './public/*.html',
    './app/helpers/**/*.rb',
    './app/javascript/**/*.js',
    './app/views/**/*.{erb,haml,html,slim}'
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter var', ...defaultTheme.fontFamily.sans],
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/aspect-ratio'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/container-queries'),
  ]
}

Now, create the Stimulus controller that will handle switching to dark mode and back.

rails g stimulus theme

Now modify the controller at app/javascript/controllers/theme_controller.js

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="theme"
export default class extends Controller {
  toggleDarkModeClass() {
        if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
            document.documentElement.classList.add('dark')
        } else {
            document.documentElement.classList.remove('dark')
        }
    }

    toggle() {
        if (localStorage.theme === 'dark') {
            localStorage.theme = 'light'
        } else {
            localStorage.theme = 'dark'
        }
        this.toggleDarkModeClass()
    }
}

Now, let's hook up the controller and make the switch. I have this inside my nav bar, but you can put wherever you need. I also moved all of this to a partial that I render to help keep the code clean. Style as you wish, just be sure the data controller and action are maintained.

Remember from the beginning, I am using Font Awesome icons below with the method provided by their gem.

<div data-controller="theme" class="mt-1 mr-4">

  <div class="block dark:hidden">
  <%= icon('fa-solid', 'moon', class: "text-xl cursor-pointer text-slate-300 hover:text-cyan-400", data: { action: 'click->theme#toggle'} )%>
  </div>

  <div class="hidden dark:block">
  <%= icon('fa-solid', 'sun', class: "text-xl cursor-pointer text-slate-300 hover:text-cyan-400 hidden", data: { action: 'click->theme#toggle'} )%>
  </div>

</div>

The final piece is to do add a dark mode check in the header of your application file. This will set the theme based on user preference and also prevent flashing. I also moved this to a partial but you can add straight to the head tag if you choose.

<script>
  // Dark Mode Check
  if (localStorage.theme === 'dark' || (!('theme' in localStorage) && 
  window.matchMedia('(prefers-color-scheme: dark)').matches)) {
  document.documentElement.classList.add('dark')
  }
</script>

Now you should have a dark mode switch that allows the user to manually choose their preference.

Comments

You must be logged in to comment.