🌘

How to Add Dark Mode in Next.js 16 with Tailwind CSS v4 & JavaScript

Learn how to add dark mode in Next.js 16 using Tailwind CSS v4 and JavaScript. This step-by-step tutorial covers setting up next-themes, creating a theme toggle button, configuring Tailwind for dark mode, and integrating it in your app. Perfect for developers looking for a practical, working dark mode implementation in Next.js.

https://res.cloudinary.com/drzcgtzx8/image/upload/v1769459321/portfolio/blog/add-dark-mode-nextjs-16-tailwind-css-v4-javascript_jccuee.png
Posted by@Sujal Vanjare
Published on @
Updated on @

Adding dark mode in Next.js is very easy and simple.

Here’s a simple step-by-step guide to get started:

If you want to skip the steps, you can directly clone the repository here πŸ‘‡πŸ»

GitHub - Sujal-Vanjare/next.js-dark-mode-toggle-boilerplate-tailwind-css-javascript: A Next.js boilerplate with preconfigured dark mode using next-themes, Tailwind CSS, and JavaScript. Includes a single-click theme toggle in the navbar and a footer theme switcher styled like the official Next.js site.

A Next.js boilerplate with preconfigured dark mode using next-themes, Tailwind CSS, and JavaScript. Includes a single-click theme toggle in the navbar and a footer theme switcher styled like the of…

Logo
https://github.com/Sujal-Vanjare/next.js-dark-mode-toggle-boilerplate-tailwind-css-javascript
Thumbnail

Or Check the live demo here πŸ‘‡πŸ»

Dark Mode with Next.js

A Next.js boilerplate with preconfigured dark mode using next-themes, featuring a one-click theme toggle in the navbar and a footer theme switcher styled and behaving like the official Next.js site. Created by Sujal Vanjare.

Logo
https://nextjs-dark-mode-toggle-boilerplate.vercel.app/
Thumbnail

This guide uses javascript-logo JavaScript.

If you prefer typescript TypeScript, check out this blog πŸ‘‰πŸ» πŸŒ—How to Add Dark Mode in Next.js 16 with Tailwind CSS v4 & TypeScript


Step 1: Create a new Next.js app

1. Open VS Code or any code editor.

2. Open the terminal.

  • Windows: CTRL + `
  • macOS: CMD + `

⚠️ Before you begin, make sure you have Node.js version 20.9 or above installed on your system.

Check your version:

Bash
node -v

3. Paste the pnpm create next-app@latest dark-mode-nextjs-tailwind-css-app command in the terminal and press Enter.

(Using pnpm is highly recommended to save disk space)

Bash
pnpm create next-app@latest dark-mode-nextjs-tailwind-css-app

Using npm

Bash
npx create-next-app@latest dark-mode-nextjs-tailwind-css-app

If you already created a folder

Open that folder in VS Code and run this command in the terminal:

Using pnpm

TypeScript
npx create-next-app@latest .

Using npm

Bash
pnpm create next-app@latest .

Using . installs Next.js in the current folder.

During installation, you will see the following prompts:

Bash
√ Would you like to use the recommended Next.js defaults? » No, customize settings
√ Would you like to use TypeScript? ... No / Yes
√ Which linter would you like to use? » ESLint
√ Would you like to use React Compiler? ... 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 customize the import alias (`@/*` by default)? ... No / Yes
√ What import alias would you like configured? ... @/*
Creating a new Next.js app in ..

What to select:

  • Choose β€œNo, customize settings” when asked about recommended Next.js defaults (use ↑ ↓ arrow keys)
  • Choose No for TypeScript (use ← β†’ arrow keys)
  • Choose Yes for ESLint (optional but recommended)
  • Choose No for React Compiler
  • Choose Yes for Tailwind CSS
  • Choose Yes to use the src/ directory
  • Choose Yes for App Router
  • Choose Yes to customize the import alias
  • When asked for the import alias, just press Enter to use the default @/*

That’s it. The project will now install.

Open the created project directly in VS Code

if you install through directly inside folder, then skips this , After the installation completes, you may be in the C or D drive, or any other folder or directory.

To open the created project in VS Code, run the command below in same terminal

Bash
code dark-mode-nextjs-tailwind-css-app -r
If your folder name is different, replace dark-mode-nextjs-tailwind-css-app with your own folder name.

Step 2: Install the next-themes package

next-themes

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

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

This is a lightweight and well-maintained package that is widely used.

Open the terminal, run the command below.

Bash
pnpm i next-themes

Using npm

Bash
npm i next-themes

Step 3: Add ThemeProvider from next-themes in layout.jsx

πŸ“Œ

Rename the existing layout.js file to layout.jsx.

You can do this by:

  • Selecting the file, right-clicking it, and choosing Rename
  • Or selecting the file and pressing F2

We use .jsx because the file contains JSX syntax (HTML-like markup). Using .jsx makes it clear the file renders UI, not just plain JavaScript.

In the root layout.jsx file, import ThemeProvider

JavaScript
import { ThemeProvider } from "next-themes";

Wrap {children} with ThemeProvider

JavaScript
<ThemeProvider
  attribute="class"
  defaultTheme="system"
  enableSystem
  disableTransitionOnChange
>
  <Navbar />
  {children}
  <Footer />
</ThemeProvider>;

If you have other components like Navbar or Footer, make sure they are also wrapped inside it (the whole content), but not the <html> or <body> tags.

❌

Don’t wrap the <body> or <html> with ThemeProvider

In the App Router, <html> and <body> are required structural elements and must stay at the top level of layout.jsx. Wrapping them with a Client Component like ThemeProvider breaks the document structure and can cause hydration issues.

next-themes modifies the <html> element (adds class="dark" or a data attribute) on the client. If React is also trying to manage those same elements, it can lead to mismatches during hydration.

The correct pattern is to keep <html> and <body> static, add suppressHydrationWarning to <html>, and wrap only the content inside <body> with ThemeProvider.

Don’t worry! Wrapping your entire app with ThemeProvider (which is a Client Component) does not turn all child components into Client Components.

This works because of a pattern called Composition, also known as the β€œchildren exception”.

In Next.js, when you pass components as children to a Client Component, they keep their original type (Server or Client).


Important

Make sure to add suppressHydrationWarning on the <html> tag, like this πŸ‘‡πŸ»

HTML
<html lang="en"suppressHydrationWarning>

Why we add this?

The theme is resolved on the client, not the server.

Without this, you may see hydration warnings because the server-rendered theme and client-rendered theme can be different.


Theme Provider props Explanation:

  • attribute="class"

    Tells next-themes how to apply the theme.

    With this option, it adds a dark class to the <html> element when dark mode is active.

    This is required when using Tailwind CSS, because Tailwind’s dark mode works by checking for the .dark class.

    Without attribute="class", Tailwind dark: utilities will not work with the theme toggle.

  • defaultTheme="system"

    Sets the default theme based on the user’s system preference.

    You can override this with defaultTheme="light" or defaultTheme="dark".

    On the first render, the website will use this theme.

  • enableSystem

    Allows automatic switching between dark and light mode based on the system theme.

    Only use this when defaultTheme="system".

    If the default theme is light or dark, don’t add this prop.

  • disableTransitionOnChange

    Disables all CSS transitions when switching themes to avoid flicker.

    ⚠️

    This is very important and most people skip it.

    If you don’t add this and you have transitions on background or colors, those transitions will run when the theme changes.


Just copy-paste the file below into your layout.jsx.

Don’t worry about the Navbar component error, we will add it in the next step.

JavaScript
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "next-themes";
import Navbar from "@/ui/navbar";

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

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

export const metadata = {
  title: "Dark Mode with Next.js",
  description:
    "A Next.js boilerplate with preconfigured dark mode using next-themes, featuring a one-click theme toggle in the navbar and a footer theme switcher styled and behaving like the official Next.js site. Created by Sujal Vanjare.",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <Navbar />
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
src/app/layout.jsx

Step 4: Add theme toggle button in the navbar

Create a ui folder inside the src folder.

Inside the ui folder, create a theme-toggle.jsx file.

I have given you a best-practice theme toggle button that:

  • Waits for the component to mount
  • Uses the resolved theme
  • Sets a dynamic title and aria-label
  • Uses aria-pressed for toggle semantics
  • Disables interaction until the theme is ready

Just copy and paste the code below πŸ‘‡πŸ» into src/ui/theme-toggle.jsx.

JavaScript
"use client";

import cn from "@/utils/class-merge";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

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

  useEffect(() => {
    setMounted(true);
  }, []);

  // Only compute theme after mount
  const effectiveTheme =
    mounted && theme === "system" && resolvedTheme
      ? resolvedTheme
      : mounted
        ? theme
        : null;

  const nextTheme = effectiveTheme === "dark" ? "light" : "dark";

  const label = effectiveTheme ? `Switch to ${nextTheme} mode` : "Toggle theme";

  const toggleTheme = () => {
    if (!mounted || !nextTheme) return;
    setTheme(nextTheme);
  };

  return (
    <button
      type="button"
      title={label}
      aria-label={label}
      aria-pressed={effectiveTheme === "dark"}
      disabled={!mounted}
      onClick={toggleTheme}
      className={cn(
        "shrink-0 flex items-center justify-center p-3 rounded-full border border-solid transition-colors",
        "border-black/8 dark:border-white/14",
        mounted
          ? "cursor-pointer hover:border-transparent hover:bg-black/4 dark:hover:bg-[#1a1a1a]"
          : "cursor-default pointer-events-none",
      )}
    >
      {/* Sun Icon */}
      <svg
        viewBox="0 0 16 16"
        width="24"
        height="24"
        className="w-6 h-6 dark:hidden"
      >
        <g clipPath="url(#clip0_53_17725)">
          <path
            fill="currentColor"
            fillRule="evenodd"
            d="M8.75.75V0h-1.5v2h1.5V.75M3.26 4.32l-.53-.53-.35-.35-.53-.53L2.9 1.85l.53.53.35.35.53.53zm8.42-1.06.53-.53.35-.35.53-.53 1.06 1.06-.53.53-.35.35-.53.53zM8 11.25a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m0 1.5a4.75 4.75 0 1 0 0-9.5 4.75 4.75 0 0 0 0 9.5m6-5.5h2v1.5h-2zm-13.25 0H0v1.5h2v-1.5H.75m1.62 5.32-.53.53 1.06 1.06.53-.53.35-.35.53-.53-1.06-1.06-.53.53zm10.2 1.06.53.53 1.06-1.06-.53-.53-.35-.35-.53-.53-1.06 1.06.53.53zM8.75 14v2h-1.5v-2z"
            clipRule="evenodd"
          ></path>
        </g>
        <defs>
          <clipPath id="clip0_53_17725">
            <rect width="16" height="16" fill="white"></rect>
          </clipPath>
        </defs>
      </svg>

      {/* Moon Icon */}
      <svg
        viewBox="0 0 16 16"
        width="24"
        height="24"
        className="hidden w-6 h-6 dark:block"
      >
        <path
          fill="currentColor"
          fillRule="evenodd"
          d="M1.5 8a6 6 0 0 1 3.62-5.51 7 7 0 0 0 7.08 9.25A5.99 5.99 0 0 1 1.5 8M6.42.58a7.5 7.5 0 1 0 7.96 10.41l-.92-1.01a5.5 5.5 0 0 1-6.3-8.25zm6.83.42v1.75H15v1.5h-1.75V6h-1.5V4.25H10v-1.5h1.75V1z"
          clipRule="evenodd"
        ></path>
      </svg>
    </button>
  );
}
src/ui/theme-toggle.jsx

Add cn utility function (required for the theme toggle)

Inside the src folder, create a utils folder.

Inside the utils folder, create a class-merge.js file.

This utility is required for the theme toggle to merge Tailwind classes correctly.

First, install the required dependencies:

Using pnpm:

Bash
pnpm i clsx tailwind-merge

Using npm:

Bash
npm install clsx tailwind-merge

Now, copy and paste the code below πŸ‘‡πŸ» into src/utils/class-merge.js

JavaScript
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export default function cn(...inputs) {
  const classNames = twMerge(clsx(...inputs));
  return classNames || undefined; // Return undefined when classNames is empty to avoid rendering the class attribute.
}
src/utils/class-merge.js

Step 5: Creating Navbar and Adding Theme Toggle

In the ui folder, create a navbar.jsx file.

This navbar includes the theme toggle button you created in the previous step. Copy and paste the following code πŸ‘‡πŸ»

JavaScript
import Image from "next/image";
import ThemeToggle from "./theme-toggle";

export default function Navbar() {
  return (
    <nav className="bg-white dark:bg-black text-black dark:text-zinc-50 sticky top-0 z-10 h-20 w-full border-b border-b-solid border-b-black/8 dark:border-b-white/14">
      <div className="flex max-w-3xl h-full mx-auto px-16 justify-between items-center">
        <Image
          className="dark:invert"
          src="/next.svg"
          alt="Next.js logo"
          width={100}
          height={20}
          priority
        />
        <ThemeToggle />
      </div>
    </nav>
  );
}
src/ui/navbar.jsx

If you want, you can add the theme toggle in the footer instead of the navbar. It works exactly like the official Next.js site, both in appearance and behavior.


Step 6: Important - Enable Manual Dark Mode in Tailwind CSS

If you don’t add this, your light/dark mode won’t work with the theme toggle button; it will only follow the system (media) preference.

It’s very simple to fix: just add this @custom-variant dark (&:where(.dark, .dark *)); below the @import "tailwindcss"; in your globals.css file, like this πŸ‘‡πŸ»

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

This tells Tailwind to respect the .dark class applied by next-themes and makes your dark: classes work when switching themes manually.


Step 7: Add dark: Classes in Pages or Components

First, add light mode classes normally with Tailwind CSS.

For dark mode, use the dark: prefix to apply classes that should only be active when dark mode is on.

Example:

CSS
bg-white dark:bg-black

In your page.jsx, you can use it like this πŸ‘‡πŸ»

JavaScript
export default function Home() {
  return (
    <div className="flex min-h-[calc(100dvh-80px-205px)] items-center justify-center font-sans bg-white dark:bg-black">
      <main className="flex w-full max-w-3xl flex-col items-center justify-between py-32 px-16 sm:items-start">
        <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
          <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
            To get started, edit the page.tsx file.
          </h1>
          <p className="max-w-100 text-lg leading-8 text-zinc-600 dark:text-zinc-400">
            Looking for a starting point or more instructions? Head over to{" "}
            <a
              target="_blank"
              rel="noopener noreferrer"
              href="https://vercel.com/templates?framework=next.js"
              className="font-medium text-zinc-950 dark:text-zinc-50"
            >
              Templates
            </a>{" "}
            or the{" "}
            <a
              target="_blank"
              rel="noopener noreferrer"
              href="https://nextjs.org/learn"
              className="font-medium text-zinc-950 dark:text-zinc-50"
            >
              Learning
            </a>{" "}
            center.
          </p>
        </div>
      </main>
    </div>
  );
}
src/app/page.jsx

Step 8: Start Your Development Server and Test It

Open the terminal, run the command below.

Shell
pnpm dev

Using npm

Shell
npm run dev

Then check your site at http://localhost:3000 and test the dark/light toggle.


Done! πŸŽ‰ You’ve Successfully Added Dark Mode in Next.js

Creating detailed tutorials like this takes a lot of time, and I don’t run any Google ads on my blog so you can follow along easily without distractions.

If this guide helped you and you’d like to support my work and future tutorials, please consider:

Every bit of support helps me create more high-quality tutorials just like this. Thank You ❀️ for your support.