icon

How to Add Dark Mode in Next.js 15 App Router with Tailwind CSS V4 (Latest Method)

Learn how to implement dark and light mode in your Next.js 15 App Router project using Tailwind CSS V4. This step-by-step guide covers the latest changes in Tailwind V4 and Next.js 15 for a seamless theme switcher!

thumbnail image
4 min
Posted by :Sujal
Published on
Updated on

If you already have a Next.js project, skip to the Step 1 section.

Step 1: Create a New Next.js Project

To install Next.js 15 with the App Router and the latest Tailwind CSS v4, follow these steps:

  1. 1.
    Create a project folder anywhere on your drive.
  2. 2.
    Open the folder in VS Code.
  3. 3.
    Open the terminal and run the following command:

For npm users:

Shell
Copy
npx create-next-app@latest .

For pnpm users:

Shell
Copy
pnpx create-next-app@latest .

During installation, you’ll be prompted with options:

Shell
Copy
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, follow the next section.


Step 2: Upgrade to Tailwind CSS v4

To upgrade Tailwind CSS to v4, run the following command in your terminal:

For npm users:

Shell
Copy
npx @tailwindcss/upgrade

For pnpm users:

Shell
Copy
pnpx @tailwindcss/upgrade

If you encounter errors, try forcing the upgrade:

Shell
Copy
npx @tailwindcss/upgrade --force
Shell
Copy
pnpx @tailwindcss/upgrade --force

This will automatically update your PostCSS configuration and replace outdated Tailwind CSS classes.

For more details, check the official ⤵️

Upgrade guide - Getting started

Upgrading your Tailwind CSS projects from v3 to v4.

Logo
https://tailwindcss.com/docs/upgrade-guide
Thumbnail


Step 3: Manually Install Tailwind CSS v4 ( If Upgrade Fails )

If the upgrade fails, manually install Tailwind CSS v4 by running:

Shell
Copy
npm install tailwindcss @tailwindcss/postcss postcss
Shell
Copy
pnpm install tailwindcss @tailwindcss/postcss postcss

For pnpm:

Update Your postcss.config.mjs or postcss.config.js

Replace the contents with:

JavaScript
Copy
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

export default config;
postcss.config.mjs

Modify Your Global CSS File

Remove the old Tailwind imports:

old global.css

CSS
Copy
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/global.css

And replace them with ⤵️

CSS
Copy
@import "tailwindcss";
src/app/global.css

For more details, visit the ⤵️

Install Tailwind CSS with Next.js

Setting up Tailwind CSS in a Next.js project.

Logo
https://tailwindcss.com/docs/installation/framework-guides/nextjs


Step 4: Enable Dark Mode in Tailwind CSS v4

Why is the dark class not working?

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 your global CSS file.

Solution:

Add the following to your global CSS file:

CSS
Copy
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
src/app/global.css

This will detect the .dark class and apply 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:

next-themes

An abstraction for themes in your React app.. Latest version: 0.4.4, last published: 3 months ago. Start using next-themes in your project by running `npm i next-themes`. There are 956 other projects in the npm registry using next-themes.

Logo
https://www.npmjs.com/package/next-themes
Thumbnail

For npm:

Shell
Copy
npm install next-themes

For pnpm:

Shell
Copy
pnpm install next-themes

Create a Theme Provider Component

  1. 1.
    Inside your src folder, create a new folder called theme.
  2. 2.
    Inside theme, create a file named theme-provider.jsx (or .tsx for TypeScript).
  3. 3.
    Add the following code:

For .jsx files:

JavaScript
Copy
"use client";

import { ThemeProvider as NextThemesProvider } from "next-themes";

export default function ThemeProvider({ children, ...props }) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
src/theme/theme-provider.jsx

For .tsx files ( If you are using TypeScript ):

TypeScript
Copy
"use client";

import {
  ThemeProvider as NextThemesProvider,
  ThemeProviderProps,
} from "next-themes";

export default function ThemeProvider({
  children,
  ...props
}: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
src/theme/theme-provider.jsx

Add Theme Provider in the Root Layout

Open your layout.jsx or layout.tsx file (usually located in the root folder), and modify it as follows:

For .jsx files:

JavaScript
Copy
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 you are using TypeScript ):

TypeScript
Copy
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>
  );
}
src/app/layout.jsx
⚠️

Make sure to import the ThemeProvider component from the one we created, not from next-theme.

⚠️

Make sure to add the suppressHydrationWarning attribute inside the <html> tag to prevent React hydration errors when switching themes.

Theme Provider props Explanation:

  • 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 preferences.
  • disableTransitionOnChange: Disables all CSS transitions when switching themes to ensure a flicker-free experience.

Create a Dark & Light Mode Toggle Button

  1. 1.
    Inside the theme folder, create theme-toggle.jsx (or .tsx for TypeScript).
  2. 2.
    Add the following code:
JavaScript
Copy
"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>
  );
}
src/theme/theme-toggle.jsx

Add the Toggle Button to Your Navbar

Integrate the ThemeToggle component into your navigation bar or any desired location:

JavaScript
Copy
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 Website</h1>
        <ThemeToggle />
      </div>
    </header>
  );
}
src/component/navbar.jsx

Update the home page

In your home page paste this code

JavaScript
Copy
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">♥️ Hii</h1>
    </main>
  );
}
src/app/page.jsx

Common Ways to Add Dark Mode Styles

There are two common ways to apply dark mode styles in Tailwind CSS:

1. Using dark: Classes

Tailwind CSS provides the dark: variant, which allows you to apply styles when dark mode is active.

For example:

HTML
Copy
<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 the background and text colors adapt automatically 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 across your styles.

For example:

CSS
Copy
@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);
  }
}

We extended Tailwind’s color system by defining custom properties for shadows, text, background, and border colors using CSS variables. These variables are set in :root for light mode and updated inside .dark for dark mode.

Now, we can hover-background bg-active-background button-border-color use them in our components like this:

HTML
Copy
   <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, the styles update automatically without needing dark: classes everywhere.

Updated Files for Dark Mode in Next.js

Plain Text
Copy
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 Your Development Server and Test It

For npm users:

Shell
Copy
npm run dev

For pnpm users:

Shell
Copy
pnpm dev

Visit your localhost and toggle between dark and light modes using the button. Ensure the theme persists across pages and correctly follows the system theme on the first load.


🎉 Congratulations! You've successfully implemented a seamless dark and light mode toggle in Next.js 15 and Tailwind CSS v4.

👉🏻

Check out my other blogs as well!

iconHow to Safelist Classes in Tailwind CSS V4

iconHow to Highlight Code Syntax with PrismJS Statically in Next.js with Dark and Light Themes

iconHow to add google analytics in Next.js 15 app router ( simple method )