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.

· 4 min read
Mary Maina

Mary Maina

I am a frontend engineer passionate about building intuitive user interfaces. Tools: React, Next.JS, TypeScript, React Native, NodeJs

topics

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.

Image

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 the document.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 the value object containing the current theme and the toggleTheme 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>
);

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

Image

How Light mode looks:

Image

Cheers!!!

Github repo: https://github.com/kodaschool/theme-switcher-react

share

Mary Maina

I am a frontend devloper