Dark Mode
by Julian Rubisch 
To install this controller, run this command in your Rails app directory in the terminal:
bin/rails app:template LOCATION=https://betterstimulus.com/templates/theming/dark-mode
Usage
Configuration
| Attribute | Default | Description | Required |
|---|---|---|---|
data-dark-mode-light-class |
- | Name of the light theme class to be toggled on the <html> element |
yes |
data-dark-mode-dark-class |
- | Name of the dark theme class to be toggled on the <html> element |
yes |
Implementation
The core of the implementation is a colorScheme Stimulus value, that is set both when the controller initializes, or the toggle action is called.
After it is set, the current color scheme is stored in localStorage and a custom change event is being dispatched.
This event is caught in the dark-mode:change->dark-mode#updateColorScheme action put on the body tag. This method finally toggles the supplied light/dark classes on the html element.
The rationale for this architecture is that this scheme change event can then be listened for and utilized elsewhere (see below).
Notable prior art:
Customization
First of all, the controller assumes that the theme CSS class is attached to the root <html> element. Most CSS frameworks adhere to this, but if yours is different, you’ll need to make adjustments.
Second, depending on how you implement your theme switcher widget, you’ll want to customize the toggle method. This implementation assumes that it’s a radio group and uses its value attribute.
Finally, this controller does not handle updating of the widget, since that would be beyond its responsibility. It does emit the change event though (both on connect and whenever the theme value is changed), so you can listen for this event in any other Stimulus, controller, e.g.:
<input type="radio" name="theme" data-action="dark-mode:change->radio#updateSelection" />
Gotchas
Implementing this controller will still lead to a flash of white when opening a page. That’s because of Stimulus’ lifecycle that only starts once the DOM has been rendered and parsed.
To overcome this, add a blocking script like this to your application layout:
function isDark(colorScheme) { if (colorScheme === "auto") { return window.matchMedia("(prefers-color-scheme: dark)").matches; } return colorScheme === "dark"; } if("colorScheme" in localStorage) { document.documentElement.classList.toggle("sl-theme-dark", isDark(localStorage.colorScheme)); document.documentElement.classList.toggle("sl-theme-light", !isDark(localStorage.colorScheme)); }