πŸŒ—

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

Learn how to add dark mode in Next.js 16 using Tailwind CSS v4 and TypeScript. 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/v1769420532/portfolio/blog/add-dark-mode-nextjs-16-tailwind-css-v4-typescript_lpy3bl.png
Posted by@Sujal Vanjare
Published 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-typscript: A Next.js boilerplate with preconfigured dark mode using next-themes, Tailwind CSS, and TypeScript. 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 TypeScript. 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-typscript
Thumbnail
A Next.js boilerplate with preconfigured dark mode using next-themes, Tailwind CSS, and TypeScript. Includes a single-click theme toggle in the navbar and a footer theme switcher styled like the official Next.js site.

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 typescript TypeScript.

If you prefer javascript-logo JavaScript, check out this blog πŸ‘‰πŸ» 🌘How to Add Dark Mode in Next.js 16 with Tailwind CSS v4 & JavaScript


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 --yes command in the terminal and press Enter.

⚠️

Note: We are using --yes to skip all prompts by using saved preferences or defaults.

The default setup enables TypeScript, Tailwind CSS, ESLint, App Router, and Turbopack, with the import alias @/*.

(Using pnpm is highly recommended to save disk space)

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

Using npm

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

If you already created a folder

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

Bash
pnpm create next-app@latest . --yes

Using . installs Next.js in the current folder.

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.tsx

In the root layout.tsx file, import ThemeProvider

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

Wrap {children} with ThemeProvider

TypeScript
<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.tsx. 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.tsx.

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

TypeScript
import type { Metadata } from "next";
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: 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,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <Navbar />
          {children}
          <Footer />
        </ThemeProvider>
      </body>
    </html>
  );
}
src/app/layout.tsx

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.tsx 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.tsx.

TypeScript
"use client";

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

export default function ThemeToggle(): JSX.Element {
  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.tsx

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.ts 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.ts

TypeScript
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

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

Step 5: Creating Navbar and Adding Theme Toggle

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

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

TypeScript
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.tsx

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.tsx, you can use it like this πŸ‘‡πŸ»

TypeScript
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.tsx

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.