Next.js + TailwindCSS v4: How to Add Dark/Light Theme with Next-Themes
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
| Attribute | Description |
|---|---|
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:
ThemeProvideris 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-variantline makes Tailwind’sdark: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:
- A Next.js project set up with next‑themes.
- Tailwind v4 configured to respect
data-themeattributes. - Custom CSS variables for light/dark backgrounds.
- 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();
| Variable | Description |
|---|---|
theme | Shows the currently selected theme ('light' or 'dark'). |
setTheme | Setter function that lets you change the theme. |
resolvedTheme | Detects 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
resolvedThemeis'dark', the function switches to'light'. - If
resolvedThemeis'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
- Install
next-themes. - Wrap your app with
<ThemeProvider>. - Configure
global.csswith the@custom-variant dark(or Tailwind’sdark:variant). - Add the theme toggler button using the
useThemehook. - Enjoy smooth dark/light theme switching! 🎉