Every component and theme ships light and dark values. Dark mode turns on when
the dark class is on an ancestor, usually <html>. Tailwind's dark:
variant uses the same class:
@custom-variant dark (&:is(.dark *));next-themes sets the class before the first paint, follows the system
setting and remembers the choice.
pnpm add next-themes
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider(
props: React.ComponentProps<typeof NextThemesProvider>
) {
return <NextThemesProvider {...props} />
}export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}suppressHydrationWarning is needed because the class is set before React
hydrates.
"use client"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ModeToggle() {
const { resolvedTheme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="sm"
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
>
Toggle theme
</Button>
)
}For an animated switch, install the Animated Theme Toggler.
Outside Next.js, toggle the class yourself and store the choice:
const root = document.documentElement
root.classList.toggle("dark", localStorage.theme === "dark")Run it in a small inline script in <head> so the page never flashes the
wrong theme.