How to build a theme switcher in React with context API
This article demonstrates how to achieve dark/light mode using React, and Tailwind with Context API. Tailwind CSS out of the box supports dark mode.
Why do you need a theme switcher?
Most applications have embraced dark mode to meet diverse preferences and user needs among other benefits. A theme switcher element provides users with the ability to switch dark and light themes. By offering a theme switcher, applications can accommodate a wider range of users and ensure that everyone can comfortably use the app regardless of their visual needs.
Prerequisites.
Before diving into the demo it is important to have the following:
- Install a React project.
- Install TailwindCSS
We shall scaffold our React application using vite
yarn create vite@late theme-switcher -- --template react-ts
cd my-project
Tailwind installation
Install Tailwind and its peer dependencies
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
Configure your tailwind.config.js file.
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Add the Tailwind directives into your index.css file by including the appropriate @tailwind directives for each layer.
//index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
After the project cleanup, we shall create different folders to support the dark/light mode feature.
Context API setup
We will use context API to ensure we can pass data through the component tree without having to pass down props manually at every level. In this case, the useThemeContext provider ensures all the components have access to the current theme mode based on the user's selection.
Here is the useThemeContext.ts file
import {
ReactNode,
createContext,
useCallback,
useContext,
useState,
} from "react";
interface ThemeContextType {
theme: boolean;
toggleTheme: () => void;
}
interface IProps {
children: ReactNode;
}
export const ThemeContext = createContext<ThemeContextType | null>(null);
const ThemeContextProvider = ({ children }: IProps) => {
const [theme, setTheme] = useState(false);
const toggleTheme = useCallback(() => {
setTheme((prev) => !prev);
document.body.classList.toggle("dark");
}, [theme]);
const value = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
};
export default ThemeContextProvider;
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error("Context must be used within a context provider");
}
return context;
};
Context creation
ThemeContext
: Creates a new context with an initial value of null
. It's typed according to the ThemeContextType
Component definition
ThemeContextProvider
: This functional component serves as the provider for the theme context.
- It takes
children
as props and manages the theme state. - It defines the
toggleTheme
function to toggle between light and dark themes. - It toggles a
"dark"
class on thedocument.body
element to apply dark mode styles globally. The dark class is provided by Tailwind to handle dark mode. This shall be explained further as we proceed. - It wraps its children in the
ThemeContext.Provider
, passing down thevalue
object containing the current theme and thetoggleTheme
function.
Accessing context
- The useTheme custom hook defines an easier for components to consume the theme context.
- It also provides a way to handle potential errors.
We wrap the App component with the theme Provider so that every component can access the store context.
We also import Navbar in the main component to ensure it is part of the main layout and rendered alongside other components.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import Navbar from "./components/Navbar.tsx";
import ThemeContextProvider from "./context/useThemeContext.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeContextProvider>
<Navbar />
<App />
</ThemeContextProvider>
</React.StrictMode>
);
Try Kodaschool for free
Click below to sign up and get access to free web, android and iOs challenges.
The Theme switcher
Tailwind has support for dark mode that lets you style your site differently when dark mode is enabled. We will use the 'selector' strategy to manually toggle between light and dark mode.
Note: The selector
strategy replaced the class
strategy in Tailwind CSS v3.4.1.
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
darkMode: "selector",
theme: {
extend: {
colors: {
primary: "#0E2439",
darkmode: "#242424",
},
},
},
plugins: [],
};
The 'dark' class gets applied whenever it is present in the HTML tree structure.
In our index.css file, we will target the body element whenever the Tailwind dark mode property is activated. This styles the body of the webpage based on the defined styles.
body.dark{
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
}
The Navbar component
This component has a theme switcher that allows users to switch between dark and light modes according to their preferences.
We use React icons to toggle the themes.
import { useTheme } from "../context/useThemeContext";
import { MdLightMode } from "react-icons/md";
import { BsFillMoonStarsFill } from "react-icons/bs";
const Navbar = () => {
const { theme, toggleTheme } = useTheme();
const handleToggle = () => {
toggleTheme();
};
return (
<nav className="flex h-20 items-center justify-between">
<h1 className="italics">BuySasa</h1>
<div
onClick={handleToggle}
className="w-[40px] h-[40px] flex items-center justify-center rounded-md border-2 border-darkmode dark:border-white"
>
{theme ? <BsFillMoonStarsFill size={20} /> : <MdLightMode size={20} />}
</div>
</nav>
);
};
export default Navbar;
In the App component, there's a product component displaying a catalog of products. It mimics an e-commerce product catalog, is updated whenever the theme switcher is toggled.
//App.tsx
import "./App.css";
import Product from "./components/Product";
function App() {
return <Product />;
}
export default App;
The Product component
The product data has been defined in a constants file. Here we import a Card component that takes in a product prop after mapping through the product list.
//Products.tsx
import { products } from "../constants/data";
import Card from "./Card";
const Product = () => {
return (
<div className="grid md:grid-cols-3 lg:grid-col-4 gap-4 my-4">
{products.map((product) => (
<Card key={product.id} product={product} />
))}
</div>
);
};
export default Product;
The Card component
The Card appearance is modified by the dark mode class.
interface Product {
product: {
id: string;
name: string;
desc: string;
cost: string;
img: string;
};
}
const Card = ({ product }: Product) => {
return (
<div className="rounded-xl border border-solid bg-white dark:bg-darkmode p-4">
<div className="flex gap-4 h-auto items-center">
<img
src={product.img}
alt={product.name}
className="w-[40px] h-[40px] rounded-full"
/>
<div>
<h1 className="text-primarydark:text-white ">{product.name}</h1>
<p className="tracking-tight text-primary dark:text-white w-[90%]">
{product.desc}
</p>
</div>
</div>
<hr className="text my-1" />
<div className="flex justify-between mt-4">
<p className="text-primary dark:text-white font-semibold">
{product.cost}
<span className="text-black dark:text-white">/month</span>
</p>
<button className="bg-primary dark:bg-darkmode">Add to Cart</button>
</div>
</div>
);
};
export default Card;
How dark mode looks
How Light mode looks:
Cheers!!!
Github repo: https://github.com/kodaschool/theme-switcher-react