Next.js + TailwindCSS v4: How to Add Dark/Light Theme with Next-Themes

Published: (December 20, 2025 at 08:53 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

TailwindCSS v4 no longer maintains multiple config files—everything now goes inside a single global.css file. This can make theming feel a bit challenging. In this guide, I’ll show you how to easily set up light, dark, and even custom themes in your Next.js project using next‑themes.

Step 01 – Initiate Your Project

👉 Install a Next.js project

pnpm create next-app my-project-name
pnpm install
pnpm dev

👉 Install Next‑Themes

pnpm add next-themes

Step 02 – Modify layout.tsx

👉 Wrap the application (children) with <ThemeProvider>

Below is a minimal layout.tsx that keeps all the default Next.js code and adds the provider.

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "next-themes";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly) {
  return (
    
      
        
          {children}
        
      
    
  );
}

👉 Add attributes for ThemeProvider


  {children}

Attributes explained

AttributeDescription
enableSystem={true}Allows the app to follow the user’s OS‑level color‑scheme preference. (false by default)
defaultTheme="system"Determines which theme loads on the first visit. Using "system" respects the OS setting.

⚠️ Hydration warning fix

If you see the warning “A tree hydrated but some attributes of the server‑rendered HTML didn’t match the client properties”, add suppressHydrationWarning to the <ThemeProvider> tag:


  
    
      {children}
    
  

Note: ThemeProvider is a client component, not a server component.

Step 03 – Customize Your Colors

👉 global.css

Add the Tailwind import and a custom variant for dark mode:

@import 'tailwindcss';

/* Enable dark variant based on data-theme attribute */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

For Tailwind v4, @import 'tailwindcss' is required.
The @custom-variant line makes Tailwind’s dark: utilities work with the [data-theme="dark"] attribute.

Example usage


  
    Next Theme With TailwindCSS v4
  

When the dark theme is active, bg-amber-500 will be applied.

👉 Define theme variables

@theme {
  /* Light mode background */
  --bg-color-light-default: hsl(220, 14%, 96%);
  /* Dark mode background */
  --bg-color-dark-default: hsl(207, 95%, 8%);
}

👉 Apply variables to the root

:root[data-theme="light"] {
  background-color: var(--bg-color-light-default);
}

:root[data-theme="dark"] {
  background-color: var(--bg-color-dark-default);
}

Now the whole application will inherit the appropriate background color based on the active theme.

Step 04 – Add a Theme Toggler Button

We’ll use the lucide‑react icon library for the toggle button.

Install icons

pnpm install lucide-react

Create ThemeTogglerBtn.tsx

'use client';

import { useTheme } } from "next-themes";
import { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";

export default function ThemeTogglerBtn() {
  const [mounted, setMounted] = useState(false);
  const { theme, setTheme, resolvedTheme } = useTheme();

  // Ensure the component only renders after hydration
  useEffect(() => setMounted(true), []);

  const toggleTheme = () => {
    setTheme(resolvedTheme === "dark" ? "light" : "dark");
  };

  if (!mounted) {
    return (
      
    );
  }

  const currentIcon =
    resolvedTheme === "dark" ? (
      
    ) : (
      
    );

  return (
    
      {currentIcon}
      
        {resolvedTheme === "dark" ? "Switch to light theme" : "Switch to dark theme"}
      
    
  );
}

You can now import ThemeTogglerBtn anywhere in your UI (e.g., in the header) to let users toggle between light and dark modes.

🎉 That’s it!

You now have:

  1. A Next.js project set up with next‑themes.
  2. Tailwind v4 configured to respect data-theme attributes.
  3. Custom CSS variables for light/dark backgrounds.
  4. A reusable client‑side toggle button.

Feel free to extend the theme system with additional custom palettes or even a “system‑only” mode. Happy coding!

Theme Switcher Button


  {currentIcon}
  Theme switcher button

useTheme Hook

import { useTheme } from 'next-themes';

const { theme, setTheme, resolvedTheme } = useTheme();
VariableDescription
themeShows the currently selected theme ('light' or 'dark').
setThemeSetter function that lets you change the theme.
resolvedThemeDetects the system‑preferred theme (the theme that is actually active on the user’s device).

mounted State

When using next-themes in a Next.js app, the theme value can differ between server‑side rendering (SSR) and client‑side hydration. To avoid a mismatch, we track a mounted flag that becomes true only after the component has mounted on the client.

import { useState, useEffect } from 'react';

const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true);
}, []);
  • Initially: mounted = false (SSR).
  • After client mount: mounted = true.

This ensures the theme toggler runs only on the client side.

Theme Toggler Function

const toggleTheme = () => {
  setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
};
  • If resolvedTheme is 'dark', the function switches to 'light'.
  • If resolvedTheme is 'light', it switches to 'dark'.

Icon Selection

import { Sun, Moon } from 'react-feather';

const currentIcon = resolvedTheme === 'dark' ? (
  
) : (
  
);
  • Dark mode: renders a Sun icon.
  • Light mode: renders a Moon icon.

Button UI


  {currentIcon}
  Theme switcher button
  • Clicking the button triggers toggleTheme.
  • {currentIcon} displays the appropriate icon.
  • The provides an accessible label for screen readers.

✅ At a Glance

  1. Install next-themes.
  2. Wrap your app with <ThemeProvider>.
  3. Configure global.css with the @custom-variant dark (or Tailwind’s dark: variant).
  4. Add the theme toggler button using the useTheme hook.
  5. Enjoy smooth dark/light theme switching! 🎉
Back to Blog

Related posts

Read more »

You Are Using TailwindCSS Wrong

I’ve mentioned before why I generally do not recommend using Tailwind CSS as the primary styling approach in my projects, and I have explained that position in...