Skip to main content

Overview

The ThemeProvider component wraps the app with next-themes functionality, enabling dark mode, light mode, and system preference detection. It’s configured to use class-based theming with dark mode as the default.

Import

import { ThemeProvider } from "@/components/theme-provider"

Usage

Wrap your app in the root layout:
app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}
Add suppressHydrationWarning to the <html> tag to prevent warnings when next-themes modifies the class attribute during hydration.

Props

children
ReactNode
required
The app content to wrap with theme context

Configuration

The component is pre-configured with the following settings:
attribute
string
default:"class"
Uses CSS class (dark) for theme switching rather than data attributes
defaultTheme
string
default:"dark"
Sets dark mode as the default theme for new users
enableSystem
boolean
default:true
Enables automatic system preference detection
disableTransitionOnChange
boolean
default:false
Allows CSS transitions when theme changes for smooth color transitions

Theme Toggle Integration

Use with the ThemeToggle component to allow users to switch themes:
import { ThemeToggle } from "@/components/theme-toggle"

export function Navbar() {
  return (
    <nav>
      <div>My App</div>
      <ThemeToggle />
    </nav>
  )
}

Accessing Theme in Components

Use the useTheme hook from next-themes in any component:
"use client"

import { useTheme } from "next-themes"

export function ThemeAwareComponent() {
  const { theme, setTheme } = useTheme()

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
      <button onClick={() => setTheme("system")}>System</button>
    </div>
  )
}

Implementation Details

theme-provider.tsx:1-18
"use client"

import { ThemeProvider as NextThemesProvider } from "next-themes"
import type { ReactNode } from "react"

export function ThemeProvider({ children }: { children: ReactNode }) {
    return (
        <NextThemesProvider
            attribute="class"
            defaultTheme="dark"
            enableSystem
            disableTransitionOnChange={false}
        >
            {children}
        </NextThemesProvider>
    )
}

Theme Toggle Component

The boilerplate includes a pre-built theme toggle with light, dark, and system options:
theme-toggle.tsx:7-38
export function ThemeToggle() {
    const { theme, setTheme } = useTheme()
    const [mounted, setMounted] = useState(false)

    useEffect(() => setMounted(true), [])

    if (!mounted) return <div className="size-9" />

    const options = [
        { value: "light", icon: Sun, label: "Light mode" },
        { value: "dark", icon: Moon, label: "Dark mode" },
        { value: "system", icon: Monitor, label: "System preference" },
    ] as const

    return (
        <div className="flex items-center gap-1 rounded-full border border-border/50 bg-muted/50 p-1 backdrop-blur-sm">
            {options.map(({ value, icon: Icon, label }) => (
                <button
                    key={value}
                    onClick={() => setTheme(value)}
                    aria-label={label}
                    className={`rounded-full p-1.5 transition-colors ${
                        theme === value
                            ? "bg-background text-foreground shadow-sm"
                            : "text-muted-foreground hover:text-foreground"
                    }`}
                >
                    <Icon className="size-4" />
                </button>
            ))}
        </div>
    )
}

CSS Configuration

The theme system uses CSS variables defined in globals.css. The dark class toggles these variables:
globals.css
:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  /* ... more light theme variables */
}

.dark {
  --background: 0 0% 3.9%;
  --foreground: 0 0% 98%;
  /* ... more dark theme variables */
}

Avoiding Flash of Unstyled Content

HTML Attribute

Add suppressHydrationWarning to prevent React warnings:
<html lang="en" suppressHydrationWarning>

Server-Side Rendering

The theme toggle component handles SSR properly by:
  1. Returning a placeholder during server render (if (!mounted))
  2. Updating after client-side hydration (useEffect(() => setMounted(true), []))

Customization

Change Default Theme

<ThemeProvider defaultTheme="light">{children}</ThemeProvider>

Disable System Preference

<ThemeProvider enableSystem={false}>{children}</ThemeProvider>

Disable Transitions

Useful if theme changes cause layout shifts:
<ThemeProvider disableTransitionOnChange>{children}</ThemeProvider>

Use Data Attribute Instead of Class

<ThemeProvider attribute="data-theme">{children}</ThemeProvider>
Then update your CSS:
[data-theme="dark"] {
  /* dark theme styles */
}

Example: Full Layout Integration

From the Auth UI Boilerplate:
app/layout.tsx:22-36
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}
  • LoginRequired - Protects pages from unauthenticated access
  • Theme Toggle (built-in) - UI component for switching themes

Build docs developers (and LLMs) love