DEV Community

Cover image for The Easiest Way to Implement Theme Toggling in React 19 using next-themes & Tailwind CSS v4
Ahmed Rabee
Ahmed Rabee

Posted on

The Easiest Way to Implement Theme Toggling in React 19 using next-themes & Tailwind CSS v4

There is almost no React project today that doesn’t need at least two themes (Light and Dark). While you could build a custom theme toggle using the React Context API, it usually leads to complex boilerplate code just to handle system preferences and saving user choices.

Instead, we can use the incredibly popular NPM package next-themes. Despite the name, it works beautifully with plain React! While Next.js requires a bit more care with theming due to Server-Side Rendering (SSR) and hydration mismatches, setting this up in a React Single Page Application (SPA) is incredibly straightforward.

In this article, I will show you how to implement a seamless theme toggle using React 19, Vite, and the newly released Tailwind CSS v4.

Step 1: Create a New Vite Project

First things first, let’s create a new React project using Vite. Open your terminal and run:

pnpm create vite
# OR
npm create vite
Enter fullscreen mode Exit fullscreen mode

Follow the prompts to select React and JavaScript/TypeScript. Once that’s done, install and configure Tailwind CSS v4 according to their official documentation.

Step 2: Configure Tailwind CSS v4 for Dark Mode

Tailwind v4 is CSS-first, meaning we handle configuration right inside our CSS file. To enable class-based dark mode, we just need to define a custom variant.

Open your ./src/index.css file and set it up like this:

/* ./src/index.css */
@import "tailwindcss";
/* Add this line to enable class-based dark mode */
@custom-variant dark (&:where(.dark, .dark *));
Enter fullscreen mode Exit fullscreen mode

Step 3: Install Dependencies

Next, let’s install next-themes and a library for our theme icons (react-icons):

pnpm add next-themes react-icons
# OR
npm install next-themes react-icons
Enter fullscreen mode Exit fullscreen mode

Step 4: Create the Theme Provider

Now, let’s create a wrapper component for our theme. This provider will wrap our entire application and inject the current theme into the HTML structure.

Create a new file called ReactThemeProvider.jsx:

// ./src/ReactThemeProvider.jsx
import { ThemeProvider } from "next-themes";
export default function ReactThemeProvider({ children }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: By settingattribute="class" , we are tellingnext-themes to toggle thedark class on the<html> element, which works perfectly with our Tailwind configuration.

Step 5: Wrap Your App Component

Head over to your entry file and wrap the <App /> component with the provider we just created.

// ./src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import ReactThemeProvider from "./ReactThemeProvider.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    // wrap App component with ReactThemeProvider
    <ReactThemeProvider>
      <App />
    </ReactThemeProvider>
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Step 6: Create the Toggle Button

Finally, let’s create the button that users will click to toggle the theme. We will use the useTheme hook provided by next-themes to check the resolvedTheme (which calculates whether the system preference is currently light or dark) and switch it.

// ./components/ToggleThemeBtn.jsx
import { useTheme } from "next-themes";
import { LuSun, LuMoon } from "react-icons/lu";

export function ToggleThemeBtn() {
  const { resolvedTheme, setTheme } = useTheme();
  return (
    <button
      onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
      className="p-2 rounded-md bg-slate-200 dark:bg-slate-800 text-slate-800 dark:text-slate-200 transition-colors"
      aria-label="Toggle theme"
    >
      {resolvedTheme === "dark" ? <LuSun size={20} /> : <LuMoon size={20} />}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that is it! You now have a fully functioning, Tailwind-compatible theme toggle that respects user system preferences and saves their choices locally. Easy, right?!

Top comments (0)