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-themesCreate 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> }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> );}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> );}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> );}