Next.js

Implement dark mode in your Next.js project.

Implementation with next-themes

For Next.js, the most robust way to handle dark mode is by using the next-themes library.

Install the dependency

Install next-themes in your project using your preferred package manager:

npm install next-themes

Create the ThemeProvider

Create a theme-provider.tsx component (usually in your components/ folder) to wrap your application:

components/theme-provider.tsx
12345678910111213
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>
{children}
</NextThemesProvider>
}

Wrap your application

Add the ThemeProvider to your main layout, making sure to include the suppressHydrationWarning prop in the <html> tag:

app/layout.tsx
12345678910111213141516171819202122232425262728293031
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/theme-provider";
import "./globals.css";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
suppressHydrationWarning
>
<body className="min-h-full flex flex-col">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
</body>
</html>
);
}

Add a Theme Toggle

Add a theme toggle anywhere in your application to switch between light and dark mode:

components/theme-toggle.tsx
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
"use client";
import { useState, useEffect } from "react";
import { Sun, Monitor, Moon } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
type Theme = "light" | "system" | "dark";
const LENS_POSITIONS: Record<Theme, string> = {
light: "translate-x-0",
system: "translate-x-full",
dark: "translate-x-[200%]",
};
const THEME_OPTIONS = [
{ value: "light", icon: Sun, label: "Light theme" },
{ value: "system", icon: Monitor, label: "System theme" },
{ value: "dark", icon: Moon, label: "Dark theme" },
] as const;
export default function ThemeToggle({ className }: { className?: string }) {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
const timer = setTimeout(() => setMounted(true), 0);
return () => clearTimeout(timer);
}, []);
const currentTheme = mounted ? ((theme as Theme) || "system") : "system";
return (
<div
className={cn(
"relative border border-glass-border shadow-glass-sm w-[97px] h-8 flex items-center rounded-glass-sm",
className
)}
role="radiogroup"
aria-label="Theme toggle"
>
<div
className={cn(
"absolute w-8 h-8 rounded-glass-sm z-0 glass",
mounted ? "transition-transform duration-300 cubic-bezier(0.4, 0.0, 0.2, 1)" : "",
LENS_POSITIONS[currentTheme]
)}
/>
{THEME_OPTIONS.map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => setTheme(value)}
className={cn(
"flex z-10 h-8 w-8 rounded-glass-sm transition-colors duration-200 justify-center items-center",
currentTheme === value ? "text-foreground" : "text-muted-foreground hover:text-foreground"
)}
aria-label={label}
role="radio"
aria-checked={currentTheme === value}
>
<Icon className="h-4 w-4" />
</button>
))}
</div>
);
}