Overview
This article describes how to add dark mode using Tailwind CSS V4 with the Next.js 15 App Router.
The following article was helpful as a reference.
https://sujalvanjare.vercel.app/blog/dark-mode-nextjs15-tailwind-v4
I was able to switch between dark mode and light mode as shown below.

The following is a Japanese translation of the above article by ChatGPT.
Learn how to implement dark mode and light mode using Tailwind CSS V4 in a Next.js 15 App Router project. This step-by-step guide covers the latest changes in Tailwind V4 and Next.js 15 to achieve a seamless theme switcher!
If you already have a Next.js project, proceed to Step 1.
Step 1: Create a New Next.js Project
To install Next.js 15 with the App Router and Tailwind CSS v4, follow these steps:
- Create a project folder in any location.
- Open the folder in VS Code.
- Open a terminal and run the following command:
For npm users:
npx create-next-app@latest .
For pnpm users:
pnpx create-next-app@latest .
During installation, you will see the following options:
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`? No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
Use the arrow keys to select options and press Enter. Make sure to enable Tailwind CSS and App Router.
By default, Next.js installs Tailwind CSS v3. To upgrade, proceed to the next section.
Step 2: Upgrade to Tailwind CSS v4
To upgrade Tailwind CSS to v4, run the following command in the terminal:
For npm users:
npx @tailwindcss/upgrade
For pnpm users:
pnpx @tailwindcss/upgrade
If you encounter errors, try a forced upgrade:
npx @tailwindcss/upgrade --force
pnpx @tailwindcss/upgrade --force
This will automatically update the PostCSS configuration and replace old Tailwind CSS classes.
For more details, check the official upgrade guide.
Step 3: Manually Install Tailwind CSS v4 (If Upgrade Fails)
If the upgrade fails, run the following command to manually install Tailwind CSS v4:
npm install tailwindcss @tailwindcss/postcss postcss
For pnpm:
pnpm install tailwindcss @tailwindcss/postcss postcss
Update postcss.config.mjs or postcss.config.js
Replace the contents with:
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
Modify the Global CSS File
Remove the old Tailwind imports:
@tailwind base;
@tailwind components;
@tailwind utilities;
And replace them with:
@import "tailwindcss";
For more details, see how to install Tailwind CSS with Next.js.
Step 4: Enable Dark Mode in Tailwind CSS v4
Why the dark Class Does Not Work
In Tailwind CSS v4, the tailwind.config.js file has been removed, and the darkMode: "class" setting no longer exists. This means you need to enable dark mode directly in the global CSS file.
Solution:
Add the following to your global CSS file:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
This detects the .dark class and applies the correct dark mode styles.
Step 5: Add a Dark Mode Toggle Button
Install next-themes
To easily toggle dark mode, install the next-themes package:
For npm:
npm install next-themes
For pnpm:
pnpm install next-themes
Create a Theme Provider Component
- Create a new folder called theme inside the src folder.
- Inside the theme folder, create a file named theme-provider.jsx (or .tsx for TypeScript).
- Add the following code:
For .jsx files:
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export default function ThemeProvider({ children, ...props }) {
return NextThemesProvider {...props}>{children}NextThemesProvider>;
}
For .tsx files (if using TypeScript):
"use client";
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps,
} from "next-themes";
export default function ThemeProvider({
children,
...props
}: ThemeProviderProps) {
return NextThemesProvider {...props}>{children}NextThemesProvider>;
}
Add the Theme Provider to the Root Layout
Open your layout.jsx or layout.tsx file (usually in the root folder) and modify it as follows:
For .jsx files:
import Navbar from "../components/navbar.jsx";
import ThemeProvider from "../theme/theme-provider";
import "./globals.css";
export default function RootLayout({ children }) {
return (
html lang="en" suppressHydrationWarning>
body className="bg-white dark:bg-[#191919] text-[#37352f] dark:text-[#ffffffcf]">
ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
Navbar />
{children}
ThemeProvider>
body>
html>
);
}
For .tsx files (if using TypeScript):
import Navbar from "../components/navbar.jsx";
import ThemeProvider from "../theme/theme-provider";
import "./globals.css";
export default function RootLayout({
children,
}: Readonly
children: React.ReactNode;
}>) {
return (
html lang="en" suppressHydrationWarning>
body className="bg-white dark:bg-[#191919] text-[#37352f] dark:text-[#ffffffcf]">
ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
Navbar />
{children}
ThemeProvider>
body>
html>
);
}
Warning: Make sure you import the ThemeProvider component you created, not directly from next-themes.
Warning: Do not forget to add the suppressHydrationWarning attribute to the <html> tag to prevent React hydration errors when switching themes.
Theme provider prop descriptions:
defaultTheme="system": Sets the default theme to match the system theme.enableSystem: Allows automatic switching between dark and light modes based on the user’s system settings.disableTransitionOnChange: Disables all CSS transitions to ensure a flicker-free experience when switching themes.
Create the Dark Mode and Light Mode Toggle Button
- Create theme-toggle.jsx (or .tsx for TypeScript) inside the theme folder.
- Add the following code:
"use client";
import { useTheme } from "next-themes";
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
button
type="button"
className="cursor-pointer bg-white dark:bg-[#191919] text-[#37352f] dark:text-[#ffffffcf] hover:bg-hover-background active:bg-active-background rounded-md border border-button-border-color p-1.5 [transition:background_20ms_ease-in,_color_0.15s]"
title="Toggle theme"
aria-label="Toggle theme"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
svg
role="graphics-symbol"
viewBox="0 0 15 15"
width="15"
height="15"
fill="none"
className="w-4 h-4 dark:hidden"
>
path
d="M7.5 0C7.77614 0 8 0.223858 8 0.5V2.5C8 2.77614 7.77614 3 7.5 3C7.22386 3 7 2.77614 7 2.5V0.5C7 0.223858 7.22386 0 7.5 0ZM2.1967 2.1967C2.39196 2.00144 2.70854 2.00144 2.90381 2.1967L4.31802 3.61091C4.51328 3.80617 4.51328 4.12276 4.31802 4.31802C4.12276 4.51328 3.80617 4.51328 3.61091 4.31802L2.1967 2.90381C2.00144 2.70854 2.00144 2.39196 2.1967 2.1967ZM0.5 7C0.223858 7 0 7.22386 0 7.5C0 7.77614 0.223858 8 0.5 8H2.5C2.77614 8 3 7.77614 3 7.5C3 7.22386 2.77614 7 2.5 7H0.5ZM2.1967 12.8033C2.00144 12.608 2.00144 12.2915 2.1967 12.0962L3.61091 10.682C3.80617 10.4867 4.12276 10.4867 4.31802 10.682C4.51328 10.8772 4.51328 11.1938 4.31802 11.3891L2.90381 12.8033C2.70854 12.9986 2.39196 12.9986 2.1967 12.8033ZM12.5 7C12.2239 7 12 7.22386 12 7.5C12 7.77614 12.2239 8 12.5 8H14.5C14.7761 8 15 7.77614 15 7.5C15 7.22386 14.7761 7 14.5 7H12.5ZM10.682 4.31802C10.4867 4.12276 10.4867 3.80617 10.682 3.61091L12.0962 2.1967C12.2915 2.00144 12.608 2.00144 12.8033 2.1967C12.9986 2.39196 12.9986 2.70854 12.8033 2.90381L11.3891 4.31802C11.1938 4.51328 10.8772 4.51328 10.682 4.31802ZM8 12.5C8 12.2239 7.77614 12 7.5 12C7.22386 12 7 12.2239 7 12.5V14.5C7 14.7761 7.22386 15 7.5 15C7.77614 15 8 14.7761 8 14.5V12.5ZM10.682 10.682C10.8772 10.4867 11.1938 10.4867 11.3891 10.682L12.8033 12.0962C12.9986 12.2915 12.9986 12.608 12.8033 12.8033C12.608 12.9986 12.2915 12.9986 12.0962 12.8033L10.682 11.3891C10.4867 11.1938 10.4867 10.8772 10.682 10.682ZM5.5 7.5C5.5 6.39543 6.39543 5.5 7.5 5.5C8.60457 5.5 9.5 6.39543 9.5 7.5C9.5 8.60457 8.60457 9.5 7.5 9.5C6.39543 9.5 5.5 8.60457 5.5 7.5ZM7.5 4.5C5.84315 4.5 4.5 5.84315 4.5 7.5C4.5 9.15685 5.84315 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5C10.5 5.84315 9.15685 4.5 7.5 4.5Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
>path>
svg>
svg
role="graphics-symbol"
viewBox="0 0 15 15"
width="15"
height="15"
fill="none"
className="hidden w-4 h-4 dark:block"
>
path
d="M2.89998 0.499976C2.89998 0.279062 2.72089 0.0999756 2.49998 0.0999756C2.27906 0.0999756 2.09998 0.279062 2.09998 0.499976V1.09998H1.49998C1.27906 1.09998 1.09998 1.27906 1.09998 1.49998C1.09998 1.72089 1.27906 1.89998 1.49998 1.89998H2.09998V2.49998C2.09998 2.72089 2.27906 2.89998 2.49998 2.89998C2.72089 2.89998 2.89998 2.72089 2.89998 2.49998V1.89998H3.49998C3.72089 1.89998 3.89998 1.72089 3.89998 1.49998C3.89998 1.27906 3.72089 1.09998 3.49998 1.09998H2.89998V0.499976ZM5.89998 3.49998C5.89998 3.27906 5.72089 3.09998 5.49998 3.09998C5.27906 3.09998 5.09998 3.27906 5.09998 3.49998V4.09998H4.49998C4.27906 4.09998 4.09998 4.27906 4.09998 4.49998C4.09998 4.72089 4.27906 4.89998 4.49998 4.89998H5.09998V5.49998C5.09998 5.72089 5.27906 5.89998 5.49998 5.89998C5.72089 5.89998 5.89998 5.72089 5.89998 5.49998V4.89998H6.49998C6.72089 4.89998 6.89998 4.72089 6.89998 4.49998C6.89998 4.27906 6.72089 4.09998 6.49998 4.09998H5.89998V3.49998ZM1.89998 6.49998C1.89998 6.27906 1.72089 6.09998 1.49998 6.09998C1.27906 6.09998 1.09998 6.27906 1.09998 6.49998V7.09998H0.499976C0.279062 7.09998 0.0999756 7.27906 0.0999756 7.49998C0.0999756 7.72089 0.279062 7.89998 0.499976 7.89998H1.09998V8.49998C1.09998 8.72089 1.27906 8.89997 1.49998 8.89997C1.72089 8.89997 1.89998 8.72089 1.89998 8.49998V7.89998H2.49998C2.72089 7.89998 2.89998 7.72089 2.89998 7.49998C2.89998 7.27906 2.72089 7.09998 2.49998 7.09998H1.89998V6.49998ZM8.54406 0.98184L8.24618 0.941586C8.03275 0.917676 7.90692 1.1655 8.02936 1.34194C8.17013 1.54479 8.29981 1.75592 8.41754 1.97445C8.91878 2.90485 9.20322 3.96932 9.20322 5.10022C9.20322 8.37201 6.82247 11.0878 3.69887 11.6097C3.45736 11.65 3.20988 11.6772 2.96008 11.6906C2.74563 11.702 2.62729 11.9535 2.77721 12.1072C2.84551 12.1773 2.91535 12.2458 2.98667 12.3128L3.05883 12.3795L3.31883 12.6045L3.50684 12.7532L3.62796 12.8433L3.81491 12.9742L3.99079 13.089C4.11175 13.1651 4.23536 13.2375 4.36157 13.3059L4.62496 13.4412L4.88553 13.5607L5.18837 13.6828L5.43169 13.7686C5.56564 13.8128 5.70149 13.8529 5.83857 13.8885C5.94262 13.9155 6.04767 13.9401 6.15405 13.9622C6.27993 13.9883 6.40713 14.0109 6.53544 14.0298L6.85241 14.0685L7.11934 14.0892C7.24637 14.0965 7.37436 14.1002 7.50322 14.1002C11.1483 14.1002 14.1032 11.1453 14.1032 7.50023C14.1032 7.25044 14.0893 7.00389 14.0623 6.76131L14.0255 6.48407C13.991 6.26083 13.9453 6.04129 13.8891 5.82642C13.8213 5.56709 13.7382 5.31398 13.6409 5.06881L13.5279 4.80132L13.4507 4.63542L13.3766 4.48666C13.2178 4.17773 13.0353 3.88295 12.8312 3.60423L12.6782 3.40352L12.4793 3.16432L12.3157 2.98361L12.1961 2.85951L12.0355 2.70246L11.8134 2.50184L11.4925 2.24191L11.2483 2.06498L10.9562 1.87446L10.6346 1.68894L10.3073 1.52378L10.1938 1.47176L9.95488 1.3706L9.67791 1.2669L9.42566 1.1846L9.10075 1.09489L8.83599 1.03486L8.54406 0.98184ZM10.4032 5.30023C10.4032 4.27588 10.2002 3.29829 9.83244 2.40604C11.7623 3.28995 13.1032 5.23862 13.1032 7.50023C13.1032 10.593 10.596 13.1002 7.50322 13.1002C6.63646 13.1002 5.81597 12.9036 5.08355 12.5522C6.5419 12.0941 7.81081 11.2082 8.74322 10.0416C8.87963 10.2284 9.10028 10.3497 9.34928 10.3497C9.76349 10.3497 10.0993 10.0139 10.0993 9.59971C10.0993 9.24256 9.84965 8.94373 9.51535 8.86816C9.57741 8.75165 9.63653 8.63334 9.6926 8.51332C9.88358 8.63163 10.1088 8.69993 10.35 8.69993C11.0403 8.69993 11.6 8.14028 11.6 7.44993C11.6 6.75976 11.0406 6.20024 10.3505 6.19993C10.3853 5.90487 10.4032 5.60464 10.4032 5.30023Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
>path>
svg>
button>
);
}
Add the Toggle Button to the Navigation Bar
Integrate the ThemeToggle component into your navigation bar or any desired location:
import ThemeToggle from "../theme/theme-toggle";
export default function Navbar() {
return (
header className="bg-white dark:bg-[#191919] text-[#37352f] dark:text-[#ffffffcf] shadow-box-shadow-first sticky top-0 z-10 h-11 w-full">
div className="mx-auto flex h-full max-w-7xl justify-between items-center px-4">
h1 className="text-md font-bold">My Websiteh1>
ThemeToggle />
div>
header>
);
}
Update the Home Page
Paste the following code into the home page:
export default function Page() {
return (
main className="flex flex-col items-center text-center justify-center w-full min-w-0 min-h-[calc(100dvh-2.75rem)] mx-auto max-w-7xl">
h1 className="text-4xl font-extrabold mb-2">♥️ Hiih1>
main>
);
}
Common Methods for Adding Dark Mode Styles
There are two common methods for applying dark mode styles with Tailwind CSS:
1. Using dark: Classes
Tailwind CSS provides the dark: variant that allows you to apply styles when dark mode is active.
For example:
body className="bg-white dark:bg-[#191919] text-[#37352f] dark:text-[#ffffffcf]">
Here, the bg-white, dark:bg-[#191919], text-[#37352f], and dark:text-[#ffffffcf] classes ensure that the background color and text color automatically adapt when dark mode is enabled.
2. Using CSS Variables
Instead of adding dark: classes everywhere, you can define theme-specific variables in global.css and use them throughout your styles.
For example:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@theme {
--shadow-box-shadow-first: 0px 1px 0px var(--shadow-color);
--color-button-border-color: var(--border-color);
--color-hover-background: var(--hover-background);
--color-active-background: var(--active-background);
}
@layer base {
:root {
--shadow-color: rgba(55, 53, 47, 0.09);
--border-color: rgba(55, 53, 47, 0.09);
--hover-background: rgba(55, 53, 47, 0.06);
--active-background: rgba(55, 53, 47, 0.16);
}
.dark {
--shadow-color: rgba(255, 255, 255, 0.094);
--border-color: rgba(255, 255, 255, 0.094);
--hover-background: rgba(255, 255, 255, 0.055);
--active-background: rgba(255, 255, 255, 0.03);
}
}
By using CSS variables to define custom properties for shadows, text, backgrounds, and border colors, we have extended Tailwind’s color system. These variables are set in :root for light mode and updated within .dark for dark mode.
Now you can use them in components like this:
button
type="button"
className="cursor-pointer hover:bg-hover-background active:bg-active-background rounded-md border border-button-border-color p-1.5 [transition:background_20ms_ease-in,_color_0.15s]"
>
Since the variable values change based on the theme, styles are automatically updated without needing dark: classes everywhere.
Updated Files for Next.js Dark Mode
nextjs-app/
│── src/
│ ├── app/
│ │ ├── layout.jsx
│ │ ├── page.jsx
│ │ ├── globals.css
│ ├── components/
│ │ ├── Navbar.jsx
│ ├── theme/
│ │ ├── ThemeToggle.jsx
│ │ ├── ThemeProvider.jsx
│── package.json
│── postcss.config.js
Step 6: Start the Development Server and Test
For npm users:
npm run dev
For pnpm users:
pnpm dev
Visit localhost and use the button to switch between dark mode and light mode. Verify that the theme persists across pages and correctly follows the system theme on the initial load.
Congratulations! You have successfully implemented seamless dark mode and light mode switching with Next.js 15 and Tailwind CSS v4.