Supporting dark mode with CSS variables

Incorporating a dark mode into a website is more than just a design trend; it's a crucial aspect of creating a compelling user interface. One of the best ways to support dark mode or custom themes is by utilizing CSS variables – we can define our colors in the root selector and dynamically adjust them based on the chosen theme or mode.

Detecting User Theme Preferences

Many operating systems offer both light and dark themes, and the prefers-color-scheme CSS media feature lets you detect the current color theme.

The prefers-color-scheme CSS media feature is used to detect if a user has requested light or dark color themes. A user indicates their preference through an operating system setting (e.g. light or dark mode) or a user agent setting. – MDN
Manually Toggling Selected Theme

To add a theme toggle, we need to know what the current theme applied to the website is. For this we'll create a custom CSS data attribute and call it data-theme. Instead of using a data attribute, we could also simply use HTML classes; it's up to you to choose what you want to use to identify the current theme. Then we get the OS preferred theme using prefers-color-scheme (if any) and assign it to theme.‌‌Once the data attribute is set, we can change the values of certain CSS variables for each value of theme like below:

:root {
	--bg-color: #000;
	--txt-color: #fff;
}

/* When the theme is set to light */
html[theme="light"] {
	--bg-color: #fff;
	--txt-color: #000;
}

/* When the theme is set to dark */
html[theme="dark"] {
	--bg-color: #000;
	--txt-color: #fff;
}

body {
	background-color: var(--bg-color);
	color: var(--txt-color);
}

Here, we set default values for the --bg-color and --txt-color variables on :root. Then, we change these values based on whether the theme is set to light or dark using the html[theme=""] CSS selector. This way, we are able to set custom values for these variables based on the current theme.

For a more natural feel, we can add a transition for the properties that change when the theme is toggled:

body {
	transition-duration: 0.3s;
	transition-property: var(--bg-color), var(--txt-color);
}

When the theme is toggled, we can ensure it's continuity by storing the theme in the localStorage. This way, when the website is revisited in the future, it will remember the theme that was previously selected and display it accordingly.

Now let's write a function to toggle the theme and change the data-theme attribute with prefers-color-scheme in a React application. When the website is loaded, we'll first check for prefers-color-scheme: dark in the window media list and check if theme is set in localStorage:

const [theme, setTheme] = useState(
  localStorage.getItem("theme") ||
    (window.matchMedia("(prefers-color-scheme: dark)").matches
      ? "dark"
      : "light")
);

Then we can include a function in the component to change the current theme:

const switchTheme = () => {
  setTheme(theme == "light" ? "dark" : "light");
  localStorage.setItem("theme", theme);
};

Now when the page loads we can apply the data-theme attribute to the html element with useEffect hook like following

useEffect(() => {
  document.documentElement.setAttribute("data-theme", theme);
}, [theme]);

Here's the full App component:

import { useEffect, useState } from "react";

export function App() {
    const [theme, setTheme] = useState(
        localStorage.getItem("theme") ||
            (window.matchMedia("(prefers-color-scheme: dark)").matches
                ? "dark"
                : "light")
    );

    const switchTheme = () => {
        setTheme(theme == "light" ? "dark" : "light");
        localStorage.setItem("theme", theme);
    };

    useEffect(() => {
        document.documentElement.setAttribute("data-theme", theme);
    }, [theme]);

    return (
        <>
            <button onClick={() => switchTheme()}>Change Theme</button>
            <p>My Content</p>
        </>
    );
}

With theme and setTheme you can create buttons with dynamic icons for toggling dark mode.‌‌ If you want to access them from anywhere in the application, you could use React context or a state management library like Zustand or Redux.

Until next time!