Theme Toggle in ReactJS using CSS Variables and React Context
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:
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 initialTheme
at 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 thelocalStorage
as a key-value pair. In our case, thekey
isDINO_TV_THEME
and itsvalue
is equal totheme
. - Now, let’s break down the
if-else
statement. In both the conditions, thedocument.documentElement
returns theElement
that is the root element of thedocument
(in our case, the<html>
element). classList.add()
&classList.remove()
adds & removes class attributes on any element respectively. Hence, when thetheme
islight
, alight-mode
class is attached to the<html>
& adark-mode
class in case of adark
theme.
Points to remember:
- As our
<html>
element is attached with either alight-mode
or adark-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 forlight-mode
and the other for thedark-mode
. The logo switch is actually the image switch that is done based on thetheme
state. fill
attribute inside SVG (works only for SVG format)
A single SVG file does the work here. SVG images have afill
attribute in the<path>
element. Changing thefill
attribute changes the color of the SVG image. In our case, thefill
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!