Sam Maton

Theme switching with the CSS light-dark() function

Recently I created some base styles for commonly used HTML elements so my new projects could start with some decent baseline styles without me having to do any work. You can find these styled elements on the components page.

I decided to make use of the relatively new (baseline 2024) CSS light-dark() function to create all of my CSS variables. This allows me to handle both light and dark themes in one place without any extra CSS. For example:

    
:root {
  color-scheme: light dark;

  --primary-hue: 220;

  --bg: light-dark(
    hsl(var(--primary-hue), 10%, 90%),
    hsl(var(--primary-hue), 10%, 10%)
  );

  --surface: light-dark(
    hsl(var(--primary-hue), 10%, 95%),
    hsl(var(--primary-hue), 10%, 15%)
  );

  --surface-high: light-dark(
    hsl(var(--primary-hue), 10%, 100%),
    hsl(var(--primary-hue), 10%, 20%)
  );
}
    
  

And just like that I have light and dark themed background and surface colors, nice. Even if we stop here, users that have their system set to light or dark mode will automatically get the correct theme. But what if we want to give users the ability to switch their theme? Well, with a little bit of Javascript we can do just that. Lets start with hooking up the theme toggle button:

    
themeToggle.addEventListener("click", () => {
  const root = document.documentElement;
  const currentTheme = root.getAttribute("data-theme");

  if (currentTheme) {
    const next = currentTheme === "light" ? "dark" : "light";
    root.setAttribute("data-theme", next);
    localStorage.setItem("theme", next);
    return;
  }

  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
  const next = prefersDark ? "light" : "dark";
  root.setAttribute("data-theme", next);
  localStorage.setItem("theme", next);
});
    
  

First, this code will check to if the 'data-theme' attribute has been set on the root element, this will only be set if the user has clicked this button before. If this is set then simply just update the attrtibute to the opposite them. If not then we check to see if the user has a dark theme prference set on their system and then set the theme based on that. In both cases we also save the the theme to local storage so that we can check it again when the page is reloaded.

Now something you may have seen before is a slight flash of the wrong theme when the page is first loaded. This is because the browser will load the page and then run our Javascript to set the theme. To fix this we can add a super small script to the head of our page or base layout:

    
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
  document.documentElement.setAttribute("data-theme", storedTheme);
}
    
  

This will simply set the data-theme attribute on the root element before the page is rendered so that the correct theme is applied immediately.

Now we just need to add a little bit of CSS to handle the theme switching. Currently our light-dark() function is not respecting our data-theme attribute and is instead using the system preference. To fix this we can add a few lines of CSS to force the theme based on the data-theme attribute:

    
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}
    
  

And just like that we have a working theme switcher using CSS variables that only need to be defined once!