Next.js cookbook · Beginner

Add dark mode to Next.js without a flash

Support system/light/dark themes in a server-rendered Next.js app without hydration warnings or flashes.

Approach

Use next-themes: it injects a blocking script that sets the theme class/attribute before paint, persists the choice, and follows prefers-color-scheme. Add suppressHydrationWarning on <html>, style with CSS variables, and render theme-dependent UI only after mount.

Solution

// app/layout.tsx
import { ThemeProvider } from "next-themes";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="data-theme" defaultTheme="system" enableSystem disableTransitionOnChange>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

// components/ThemeToggle.tsx
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return <span className="toggle-placeholder" aria-hidden />;
  return (
    <select aria-label="Theme" value={theme} onChange={(e) => setTheme(e.target.value)}>
      <option value="system">System</option>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  );
}
:root { --bg: #fff; --fg: #111; color-scheme: light; }
[data-theme="dark"] { --bg: #0b1120; --fg: #e5e7eb; color-scheme: dark; }
body { background: var(--bg); color: var(--fg); }

Pitfalls

  • Reading the theme during server render (unknown on the server).
  • Hard-coded colors bypassing CSS variables.
  • Animating every color change on theme switch.

Variations

  • Tailwind dark: variants with attribute="class".

Further reading: next-themes

Reference: https://github.com/pacocoursey/next-themes

More Next.js recipes