Design Systems Hot Takes

Search

Tiny toggle

5 min read

Over at the Design Systems House website, I made a small toggle for light and dark mode. While there’s an abundance of posts describing this, I didn’t find any that made it into a compact script of a few lines. Here’s how I did it, and some additional considerations to keep in mind.

The script

This is all the JavaScript that makes the toggle work:

const LOCALSTORAGE_KEY = 'dark';
const $toggle = document.getElementById('toggle');

const { matches } = window.matchMedia('(prefers-color-scheme: dark)');
const stored = window.localStorage.getItem(LOCALSTORAGE_KEY);
$toggle.checked = stored !== null ? stored === 'true' : matches;

$toggle?.addEventListener('change', (ev) => {
    window.localStorage.setItem(LOCALSTORAGE_KEY, String(ev.target.checked));
});

This assumes that there is a <input type="checkbox" id="toggle"> element in the HTML. Let’s go over the lines:

  1. We define a constant for the localStorage key.
  2. We get the toggle element from the DOM.
  3. We check if the user has a preference for dark mode using matchMedia.
  4. We check if there’s a stored value in localStorage
  5. We set the toggle’s checked state based on the stored value or the user’s preference.
  6. We add an event listener to the toggle.
  7. When the toggle changes, we store the boolean value as a string in localStorage.

Importantly, we assume that the initial presentation is “light” and explicitly look for if there’s any setting for “dark”. The first thing we look at is if it was set as “dark” for this site using the toggle from a previous session. If not, we check what the OS preference is. This is setting the initial state of the toggle element, being checked if “dark” is picked up from either of those conditions. Then we listen for changes to the toggle and only update the localStorage value when the toggle is changed. This overrides the OS user preference when set.

The styles

Now with the baseline :has() selector, getting this to behave is pretty easy with a single selector to introduce a new expression:

body:has(#toggle:checked) {
    /* dark mode styles */
}

This is a very simple way to apply styles based on the state of the toggle. The :has() selector allows us to check if the toggle is checked and apply styles accordingly. This way, we don’t need to add any additional classes or attributes to the body element to indicate the current mode. We can just rely on the state of the toggle itself.

About system settings

When I initially made this kind of toggle for light/dark, I thought about it differently. Instead of light/dark, I wanted to toggle between “system” and “opposite of system”. In other words, if the OS was set as “dark”, the toggle would then either align to what was set in the system or be the opposite when toggled. Visually in terms of presentation, there would be no difference. If your system was set to “dark”, then by default the site would be dark. If your system was “dark” and you wanted this site to be “light”, you would switch to the “opposite of system” option.

This helps avoid the problem of setting a specific flag on the site once set. In the implementation above and at ds.house, once we store the state expected by the user for the website, there’s no way of resetting it back to the state of not being set. It’s either “light” or “dark” (or false/true in the code). With the “system” and “opposite of system” approach, you can always reset to “system” to align with the OS preference. In this way, setting false is the same as not setting anything. This is a nice way to allow users to easily reset their preference without needing to clear localStorage or have a separate “reset” button.

There’s a few problems with this, one of them being that I could never figure out a good way to explain this setting. “System” as the default is fairly straightforward, but “opposite of system” is a bit more difficult to understand. The other problem is that it doesn’t really align with the mental model of how users think about light/dark mode. They think in terms of light and dark, not “system” and “opposite of system”. So I went with the more straightforward approach of just toggling between light and dark.

If you’d rather use a <select> instead of a <input type="checkbox">, you can modify the script to accommodate that by referencing .selected instead of .checked in the appropriate places. Just make sure your HTML has matching values for localStorage and the options in your <select>. You might want this if you want to expose “system”, “light”, and “dark” as options. Kilian Valkhof recommends having all three and Bramus Van Damme explains the different possible levels of setting this information.

Knowing your audience

If you’re wondering why I opted to not expose the “light”, “dark”, and “system” options at ds.house, it’s because no person is spending a signficant amount of time on that website. Enough that they’ll visit everyday to be productive. Setting whether or not “light” or “dark” is right for this person is most likely a one-time thing specifically for a single visit. It is unlikely that a user will want the presentation of this site to change over the course of a day, unlike productivity applications like email clients where you’ll visit multiple times a day. Those experiences will expect smarter customization options.

For a small website that gets a visit every once in a while, keeping it simple might be the better approach. But it’s important to know the tradeoffs. And if you ever come up with a good word for “opposite of system”, do let me know.

The Daily Vibe