fundamentals

Next.js Theme Integration

Add dark mode to your Next.js application

Install next-themes package

Start by installing next-themes:


pnpm add next-themes

Create a Theme Provider

components/theme-provider.tsx
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 Root Layout

Add the ThemeProvider to your root layout and add the suppressHydrationWarning prop to the html tag.


app/layout.tsx
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">
			<body className="antialiased">
				<ThemeProvider
					attribute="class"
					defaultTheme="system"
					suppressHydrationWarning>
					{children}
				</ThemeProvider>
			</body>
		</html>
	)
}

Create a Theme Toggler

components/theme-toggler.tsx
"use client"
 
import { useEffect, useState } from "react"
import { Loader2, MoonIcon, SunIcon } from "lucide-react"
import { useTheme } from "next-themes"
import { Button, IconButton } from "@/components/ui/button"
 
export function ThemeToggler() {
	// This component is only rendered on the client, so we can use useEffect to set the mounted state
	// and avoid server-side rendering issues with the theme provider
	const [mounted, setMounted] = useState(false)
	const { resolvedTheme, setTheme } = useTheme()
 
	const toggleTheme = () => {
		setTheme(resolvedTheme === "light" ? "dark" : "light")
	}
 
	// useEffect only runs on the client, so now we can safely show the UI
	useEffect(() => {
		setMounted(true)
	}, [])
 
	if (!mounted) {
		return (
			<IconButton variant="outline" color="neutral" disabled>
				<Loader2 className="size-5 animate-spin" />
			</IconButton>
		)
	}
 
	return (
		<IconButton variant="outline" color="neutral" onClick={toggleTheme}>
			{resolvedTheme === "light" ? <MoonIcon /> : <SunIcon />}
		</IconButton>
	)
}

Use the Theme Toggler

components/navigation-bar.tsx
import { ThemeToggler } from "@/components/theme-toggler"
 
export function NavigationBar() {
	return (
		<nav>
			// ....
			<ThemeToggler />
		</nav>
	)
}