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)); }