DEV Community

Salma Alam-Naylor
Salma Alam-Naylor

Posted on • Originally published at whitep4nth3r.com

The best light/dark mode theme toggle in JavaScript

Learn how to build The Ultimate Theme Toggle™️ for your website using JavaScript, CSS custom properties, local storage and system settings. No framework required!


I used to disagree with light and dark mode toggles. “The toggle is the user system preferences!” I would exclaim naïvely, opting to let the prefers-color-scheme CSS media query control the theming on my personal website. No toggle. No choice. 🫠

I’ve been a dark mode user ever since it became a thing. But recently, I’ve preferred to use some websites and tools in light mode — including my personal website — whilst keeping my system settings firmly in the dark. I needed a toggle. I needed a choice! And so does everyone else.

In this post I’ll show you how I built The Ultimate Theme Toggle™️ for my website in JavaScript that:

  1. Stores and retrieves a theme preference in local browser storage,
  2. Falls back to user system preferences,
  3. Falls back to a default theme if none of the above are detected.

TL;DR: here’s the code on CodePen.

Add a data attribute to your HTML tag

On your HTML tag, add a data attribute such as data-theme and give it a default value of light or dark. In the past I’ve used the custom attribute color-mode instead of a data attribute (e.g. color-mode="light"). While this works, it’s not classed as valid HTML and I can’t find any documentation on it! Any insight on this is much appreciated. 😅

<html lang="en" data-theme="light">
    <!-- all other HTML -->
</html>
Enter fullscreen mode Exit fullscreen mode

Configure theming via CSS custom properties

In your CSS, configure your theme colours via CSS custom properties (or variables) under each value for the data-theme attribute. Note that you don’t necessarily need to use :root in combination with data-theme, but it’s useful for global properties that don’t change with the theme (shown in the example below). Learn more about the :root CSS pseudo-class on MDN.

:root {
  --grid-unit: 1rem;
  --border-radius-base: 0.5rem;
}

[data-theme="light"] {
  --color-bg: #ffffff;
  --color-fg: #000000;
}

[data-theme="dark"] {
  --color-bg: #000000;
  --color-fg: #ffffff;
}

/* example use of CSS custom properties */
body {
  background-color: var(--color-bg);
  color: var(--color-fg);
}
Enter fullscreen mode Exit fullscreen mode

Switch the data-theme attribute manually on your HTML tag, and you’ll see your theme change already (as long as you’re using those CSS properties to style your elements)!

Build a toggle button in HTML

Add an HTML button to your website header, or wherever you need the theme toggle. Add a data-theme-toggle attribute (we’ll use this to target the button in JavaScript later), and an aria-label if you’re planning to use icons on your button (such as a sun and moon to represent light and dark mode respectively) so that screen readers and assistive technology can understand the purpose of the interactive button.

<button
    type="button"
    data-theme-toggle
    aria-label="Change to light theme"
  >Change to light theme (or icon here)</button>
Enter fullscreen mode Exit fullscreen mode

Calculate theme setting on page load

Here’s where we’ll calculate the theme setting based on what I call the “preference cascade”.

Get theme preference from local storage

We can use the localStorage property in JavaScript to save user preferences in a browser that persist between sessions (or until it is manually cleared). In The Ultimate Theme Toggle™️, the stored user preference is the most important setting, so we’ll look for that first.

On page load, use localStorage.getItem("theme") to check for a previously stored preference. Later in the post, we’ll update the theme value each time the toggle is pressed. If there’s no local storage value, the value will be null.

// get theme on page load
localStorage.getItem("theme");

// set theme on button press
localStorage.setItem("theme", newTheme);
Enter fullscreen mode Exit fullscreen mode

Detect user system settings in JavaScript

If there’s no stored theme preference in localStorage, we’ll detect the user’s system settings using the window.matchMedia() method by passing in a media query string. You’ll only need to calculate one setting for the purposes of the preference cascade, but the code below shows how you can detect light or dark system settings.

const systemSettingDark = window.matchMedia("(prefers-color-scheme: dark)");
// or
const systemSettingLight = window.matchMedia("(prefers-color-scheme: light)");
Enter fullscreen mode Exit fullscreen mode

window.matchMedia() returns a MediaQueryList containing the media query string you requested, and whether it matches (true/false) the user system settings.

{
  matches: true,
  media: "(prefers-color-scheme: dark)",
  onchange: null
}
Enter fullscreen mode Exit fullscreen mode

Fall back to a default theme

Now you have access to a localStorage value and system settings via window.matchMedia(), you can calculate the preferred theme setting using the preference cascade (local storage, then system setting), and fall back to a default theme of your choice (which should be the default theme you specified on your HTML tag earlier).

We’ll run this code on page load to calculate the current theme setting.

function calculateSettingAsThemeString({ localStorageTheme, systemSettingDark }) {
  if (localStorageTheme !== null) {
    return localStorageTheme;
  }

  if (systemSettingDark.matches) {
    return "dark";
  }

  return "light";
}

const localStorageTheme = localStorage.getItem("theme");
const systemSettingDark = window.matchMedia("(prefers-color-scheme: dark)");

let currentThemeSetting = calculateSettingAsThemeString({ localStorageTheme, systemSettingDark });
Enter fullscreen mode Exit fullscreen mode

Add an event listener to the toggle button

Next, we’ll set up an event listener to switch the theme when the button is pressed. Target the button in the DOM using the data attribute (data-theme-toggle) we added earlier, and add an event listener to the button on click. The example below is quite verbose, and you might want to abstract out some of the functionality below into utility functions (which I’ve done in the example on CodePen). Let’s walk this through:

  1. Calculate the new theme as a string
  2. Calculate and update the button text (if you're using icons on your button, here's where you'll make the switch)
  3. Update the aria-label on the button
  4. Switch the data-theme attribute on the HTML tag
  5. Save the new theme preference in local storage
  6. Update the currentThemeSetting in memory
// target the button using the data attribute we added earlier
const button = document.querySelector("[data-theme-toggle]");

button.addEventListener("click", () => {
  const newTheme = currentThemeSetting === "dark" ? "light" : "dark";

  // update the button text
  const newCta = newTheme === "dark" ? "Change to light theme" : "Change to dark theme";
  button.innerText = newCta;  

  // use an aria-label if you are omitting text on the button
  // and using sun/moon icons, for example
  button.setAttribute("aria-label", newCta);

  // update theme attribute on HTML to switch theme in CSS
  document.querySelector("html").setAttribute("data-theme", newTheme);

  // update in local storage
  localStorage.setItem("theme", newTheme);

  // update the currentThemeSetting in memory
  currentThemeSetting = newTheme;
});
Enter fullscreen mode Exit fullscreen mode

To confirm localStorage is being updated, open up your dev tools, navigate to the Application tab, expand Local Storage and select your site. You’ll see a key:value list; look for theme and click the button to watch it update in real time. Reload your page and you’ll see the theme preference preserved!

Browser window with dev tools open on application tab. Local storage on whitepanther dot com is selected, showing a key value pair stored in the browser of theme light.

Put it all together!

You can now build your very own Ultimate Theme Toggle™️ by:

  1. Using CSS custom properties to specify different theme colours, switched via a data attribute on your HTML tag
  2. Using an HTML button to power the toggle
  3. Calculating the preferred theme on page load by using the preference cascade (local storage > system settings > fallback default theme)
  4. Switching the theme on click of the toggle button, and storing the user preference in the browser for future visits

Here’s the full CodePen, and you can check out the working version on my personal website. Happy toggling!

Top comments (23)

Collapse
 
thatafro profile image
Méhluli Hikwa

This is lovely, as a CSS wrangler I particularly love the text changing. Your implementation is simple and straightforward. Would love to implement it sometime maybe on my personal website which I am reworking into a nextJS site

Collapse
 
whitep4nth3r profile image
Salma Alam-Naylor

Thank you! Good luck, please share the results with me when you're done! ✨

Collapse
 
thatafro profile image
Méhluli Hikwa

I managed to implement a dark/light theme toggle on this small WordPress website.
Cyberthrust Website

Thread Thread
 
whitep4nth3r profile image
Salma Alam-Naylor

Nice! Make sure you follow some of the accessibility guidelines in the post to make sure screen readers understand what the toggle is for. Otherwise, works nicely!

Thread Thread
 
thatafro profile image
Méhluli Hikwa

Thank you for that observation totally overlooked it.

Collapse
 
nickytonline profile image
Nick Taylor

Pam from The Office saying Nice!

Collapse
 
brittanyhurthead profile image
brittanyhurthead

Definitely saving this to use later! So cool!

Collapse
 
incrementis profile image
Akin C.

Yes, pretty cool :D!
It's very nice of Salma Alam-Naylor to provide the code in Codepen to see how things work.

Collapse
 
roktim32 profile image
Roktim Kamal Senapoty

Great One @whitep4nth3r

Collapse
 
jotajeff profile image
Jeferson Silva

great job, congratulations !

Collapse
 
pazapp profile image
PaZapp

We appreciate you developers. Makes our life enjoyable. Imagine choosing from light and dark theme wow

Collapse
 
panayiotisgeorgiou profile image
Panayiotis Georgiou

awesome 🙂

Collapse
 
edenwheeler profile image
Eden Wheeler

Great post! I really enjoyed reading your insights and the way you presented your ideas was clear and engaging. Thank you for sharing your knowledge and perspective with us.

Collapse
 
whitep4nth3r profile image
Salma Alam-Naylor

Thank you so much Eden! Glad you enjoyed it ☺️

Collapse
 
sreecharan1306 profile image
Sree Charan Reddy M

Thank you.
Amazing article.
Helped me a lot

Collapse
 
whitep4nth3r profile image
Salma Alam-Naylor

You're welcome!

Collapse
 
schemetastic profile image
Schemetastic (Rodrigo)

Currently, I'm working on a website which it has this feature, and it's wild that this is almost how I am working on this feature, if I could give an extra suggestion...

Usually we would use darker colors in light backgrounds and lighter colors in darker backgrounds to provide a good contrast. For doing this, I would recommend doing something like this:

:root{
    --golden: #F5C215; /*For darker backgrounds*/
    --yellowish-brown: #9C7B0B; /*For brighter backgrounds*/

    --main-color: var(--yellowish-brown); /*assuming default theme is light*/
}

[data-theme="light"]{
    --main-color: var(--yellowish-brown); 
}
[data-theme="dark"]{
    --main-color: var(--golden); 
}
Enter fullscreen mode Exit fullscreen mode

Now, in our components, we would use the --main-color variable instead of --golden and --yellowish-brown to color our elements. So instead of adding specific colors to the elements depending on which theme is active, they would automatically change.

Collapse
 
dmitryazaraev profile image
Dmitry Azaraev

"While this works, it’s not classed as valid HTML and I can’t find any documentation on it! Any insight on this is much appreciated. 😅"

HTML represents DOM in text form. Any attribute name is valid, as long, as it is valid HTML/XML attribute name. data- convention means literally nothing than convention and treated exactly in same way as any other name. More over: all attributes handled in same way. So you must not say about validity just based on this. At least you should point in which schema or tool is considered invalid. However this is pointless, because, everything what works in browser - is perfectly valid.

Collapse
 
whitep4nth3r profile image
Salma Alam-Naylor

The W3C markup validation service claims that color-mode on HTML is invalid.

Image description

Collapse
 
georgewl profile image
George WL

I'd argue for a combination of both - use prefers-color-scheme to set the default, and then keep track of user setting

Collapse
 
whitep4nth3r profile image
Salma Alam-Naylor

But if prefers-color-scheme isn’t available for some reason, you’ll need a default anyway.

Collapse
 
georgewl profile image
George WL

Yeah, fallbacks on fallbacks, simple

Collapse
 
mahozad profile image
Mahdi Hosseinzadeh

Check out this ready to use toggle which supports system theme and changes with animation:
github.com/mahozad/theme-switch