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!