Theme Toggle in ReactJS using CSS Variables and React Context

Vedantlahane
6 min readMay 13, 2022

--

Recently, I came across the article What Is Dark Mode — And Should You Be Using It? and then realized its rapidly increasing adoption amongst the users.

I always desired to implement the dark mode functionality in my React App but never dared to start. Little did I know that it's just a few CSS variables and React Context one required to get it done.

Yes, you read it right! So without further ado, let’s get started 💪🏼

Here’s the desired output:

The desired output of a perfect Theme Toggle in ReactJS.
Desired Output

Step 1: Create CSS variables

.light-mode {
--font-color: #101212;
--body-bg-color: #ffffff;
}
.dark-mode {
--font-color: #cccccc;
--body-bg-color: #101212;
}

Firstly, create two separate classes, light-mode and dark-mode both comprising complementary colors. In simpler terms, the light-mode class should include shades of black to complement the light or shade of white in the background. On the contrary, the dark-mode class should thereby consist of shades of white to highlight the content on a black or darker background.

Note: The code only includes font-color & body-bg-color variables just to keep things simple. The color variables would eventually increase during the project development.

Step 2: Create Theme Context

Step 2.1: Import all the required hooks

import { createContext, useContext, useState, useLayoutEffect } from "react";

Don’t forget to import all of the above hooks. Each of these hooks serves a purpose in the theme generation.

Step 2.2: Create a basic context structure

const ThemeContext = createContext();const ThemeProvider = ({ children }) => {// Theme Toggle Logic starts here...return (
<ThemeContext.Provider value={} >
{children}
</ThemeContext.Provider>
);
};
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
export { ThemeProvider, useTheme };

On ThemeContext.Provider, we put the value that we want to pass down our entire component tree. We set that equal to the value prop to do so. Watch out for the value prop in the following steps.

Step 2.3: Fetch the default theme

const initialTheme = () => localStorage.getItem("DINO_TV_THEME");

Fetching the theme from the localStorage ensures that our theme is retained even after the last browsing session or a page reload.

Step 2.4: Create a theme state variable

const [theme, setTheme] = useState(initialTheme);

theme , a state variable to store the current theme is initialized with the initialThemeat first. In addition to it, we have setTheme , a state setter or updater function to switch the theme .

Step 2.5: Create a theme toggler function

const toggleTheme = () =>
setTheme((theme) => (theme === "light" ? "dark" : "light"));

The toggleTheme function assists in switching between light-mode & dark-mode using the setTheme function.

Step 2.6: Pass down theme variable & toggle function via value prop

<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>

The theme state variable and toggleTheme function is passed down the component tree using value prop.

Step 2.7: Render the Theme

useLayoutEffect(() => {
localStorage.setItem("DINO_TV_THEME", theme);
if (theme === "light") {
document.documentElement.classList.remove("dark-mode");
document.documentElement.classList.add("light-mode");
} else {
document.documentElement.classList.remove("light-mode");
document.documentElement.classList.add("dark-mode");
}
}, [theme]);

Our theme toggle approach mutates the DOM and thus preferably uses useLayoutEffect hook over useEffect hook. Also, useLayoutEffect has theme as a dependency array that will only re-run the effect when the values within the array change i.e. on the theme state change.

  • Firstly, localStorage.setItem(key, value) allows storing items in the localStorage as a key-value pair. In our case, the key is DINO_TV_THEME and its value is equal to theme.
  • Now, let’s break down the if-else statement. In both the conditions, thedocument.documentElement returns the Element that is the root element of the document(in our case, the <html>element).
  • classList.add() & classList.remove() adds & removes class attributes on any element respectively. Hence, when the theme is light , a light-mode class is attached to the <html> & a dark-mode class in case of a dark theme.
Theme Toggle

Points to remember:

  • As our <html> element is attached with either a light-mode or a dark-mode class. This creates an effect on all the DOM elements.
  • light-mode & dark-mode classes have a set of CSS variables that are applied to the respective DOM elements based on their usage.
  • Now, switching between themes is just with respect to the class name & the colors assigned to their CSS variables inside its class.

Step 2.8: Putting it all together

import { createContext, useContext, useState, useLayoutEffect } from "react";const ThemeContext = createContext();const ThemeProvider = ({ children }) => {const initialTheme = () => localStorage.getItem("DINO_TV_THEME");const [theme, setTheme] = useState(initialTheme);const toggleTheme = () =>
setTheme((theme) => (theme === "light" ? "dark" : "light"));
useLayoutEffect(() => {
localStorage.setItem("DINO_TV_THEME", theme);
if (theme === "light") {
document.documentElement.classList.remove("dark-mode");
document.documentElement.classList.add("light-mode");
} else {
document.documentElement.classList.remove("light-mode");
document.documentElement.classList.add("dark-mode");
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
export { ThemeProvider, useTheme };

Step 3: Wrap the Theme Context Provider

ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);

Wrap the ThemeProvider around the App component in the component tree.

Step 4: Using Theme Context in Navbar

const { theme, toggleTheme } = useTheme();<nav>
<ul>
<li className="header-account-link" onClick={toggleTheme}>
{theme === "dark" ? (
<span className="material-icons" title="Switch to Light Mode">
light_mode
</span>
) : (
<span className="material-icons" title="Switch to Dark Mode">
dark_mode
</span>
)}
</li>
</ul>
</nav>

Now, simply toggle between light-mode and dark-mode by switching the values inside the theme variable using toggleTheme function.

Step 5: Change logo as per the theme

<img 
className="header-logo-img"
src={
theme === "dark"
? "/assets/Logo/dino-tv-logo-dark.svg"
: "/assets/Logo/dino-tv-logo-light.svg"
}
alt="dino-tv-logo"
/>

There are two approaches to switching logos according to the theme.

  • General Method (works for any file extension)
    You’ll need two logos here. One for light-mode and the other for the dark-mode . The logo switch is actually the image switch that is done based on the theme state.
  • fill attribute inside SVG (works only for SVG format)
    A single SVG file does the work here. SVG images have a fill attribute in the <path> element. Changing the fill attribute changes the color of the SVG image. In our case, the fill attribute of the <path> element inside a LOGO.svg could be set to var( — font-color).
    Something like this,
    <path d=”M17.3681 56.3279…” fill=”var( — font-color)” />
    In this case, the logo switch is actually the — font-color variable switch that happens behind the scenes.

We have used the General Method in our case, but as our logos are in SVG format, keeping any one of those SVG logos and changing its fill attribute, the other method would also work.

That’s it! These are the steps to create a super simple, super clean theme functionality in your React App.

If you want to see a real live example, you can check out Dino TV.
Still confused? Here’s the source code of the app.

This approach is not only limited to light-theme & dark-theme but is flexible enough to cater to multiple themes. All you need to do is add CSS classes consisting of theme-related CSS variables. Refer to the Step 1 above. There’s a catch in the toggle function. It just switches between two themes as of now. For it to accommodate multiple themes, the function needs to be changed accordingly by adding if-else-if or any other control statement.

Now go ahead and add the theme you always wished for your app 🚀

Thank you for reading!

--

--