Skip to main content
This guide walks you through adding dark mode support to your Next.js application using the next-themes library, which provides excellent support for server-side rendering and prevents hydration mismatches.

Prerequisites

Before starting, ensure you have:
  • A Next.js 13+ application with App Router
  • EoN UI components installed
  • Basic understanding of React Context and client components
The next-themes library is specifically designed for Next.js and handles the complexities of SSR, preventing flash of unstyled content and hydration issues.

Implementation Steps

1

Install next-themes

First, install the next-themes package which provides a robust theme management solution for Next.js applications.
npm install next-themes
This package handles theme persistence, system preference detection, and SSR compatibility automatically.
2

Create Theme Provider Component

Create a client component that wraps the next-themes provider. This component will manage the theme state across your application.
components/theme-provider.tsx
"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>
}
The "use client" directive is required because theme management involves browser APIs like localStorage and window.matchMedia.
This wrapper component allows you to:
  • Keep the provider logic isolated
  • Add custom configuration if needed
  • Maintain type safety with TypeScript
3

Configure Root Layout

Wrap your application with the theme provider in the root layout. This ensures all components have access to the theme context.
app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head />
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Configuration Options

  • attribute="class": Applies theme as a CSS class on the HTML element (required for Tailwind’s dark mode)
  • defaultTheme="system": Uses the system preference as the default theme
  • enableSystem: Enables automatic system preference detection
  • disableTransitionOnChange: Prevents CSS transitions during theme changes for instant switching
  • suppressHydrationWarning: Prevents React hydration warnings caused by the theme script
The suppressHydrationWarning prop on the <html> tag is crucial. Without it, you’ll see hydration warnings because the theme is applied before React hydrates.
4

Add Mode Toggle Component

Create a user interface component that allows users to switch between light, dark, and system themes. You can use any EoN UI component pattern for this.Here’s an example using a dropdown menu:
components/mode-toggle.tsx
"use client"

import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function ModeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Using the Mode Toggle

Place the mode toggle component anywhere in your application, typically in the header or navigation:
components/header.tsx
import { ModeToggle } from "@/components/mode-toggle"

export function Header() {
  return (
    <header className="flex items-center justify-between p-4">
      <h1>My App</h1>
      <ModeToggle />
    </header>
  )
}

Using the Theme in Your Components

Once configured, you can access the current theme in any client component:
"use client"

import { useTheme } from "next-themes"

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

  return (
    <div>
      <p>Current theme: {theme}</p>
      <p>System theme: {systemTheme}</p>
      <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
        Toggle theme
      </button>
    </div>
  )
}

Styling Components for Dark Mode

EoN UI components automatically support dark mode when using Tailwind CSS. Use the dark: variant to apply styles in dark mode:
<div className="bg-white text-black dark:bg-slate-950 dark:text-white">
  <h2 className="text-gray-900 dark:text-gray-100">Hello World</h2>
  <p className="text-gray-600 dark:text-gray-400">This adapts to the theme</p>
</div>

Troubleshooting

Ensure you’ve added suppressHydrationWarning to the <html> tag in your root layout. This is required because next-themes injects a script that runs before React hydration.
Check that:
  • The ThemeProvider is wrapping your entire application in the root layout
  • localStorage is accessible in your environment (not blocked by privacy settings)
  • You’re not clearing localStorage elsewhere in your application
This usually happens when trying to render theme-dependent content during SSR. Use the mounted state to prevent rendering until after hydration:
"use client"

import { useEffect, useState } from "react"
import { useTheme } from "next-themes"

export function ThemeDisplay() {
  const [mounted, setMounted] = useState(false)
  const { theme } = useTheme()

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

  if (!mounted) return null

  return <div>Current theme: {theme}</div>
}

Advanced Configuration

Custom Storage Key

Change where the theme preference is stored:
<ThemeProvider storageKey="my-app-theme" attribute="class" defaultTheme="system">
  {children}
</ThemeProvider>

Multiple Themes

Support more than just light and dark:
<ThemeProvider
  attribute="class"
  defaultTheme="system"
  themes={["light", "dark", "midnight", "forest"]}
>
  {children}
</ThemeProvider>

Disable Transitions

Prevent CSS transitions during theme changes:
<ThemeProvider attribute="class" disableTransitionOnChange>
  {children}
</ThemeProvider>

Next Steps

Build docs developers (and LLMs) love