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

Dark Mode in the Wild