Published on

Implement Dark Mode in Next.js with Shadcn UI

Authors
  • Name
    Ripal & Zalak
    Twitter

Troubleshooting Dark Mode in Next.js with the App Directory

Adding dark mode functionality to your Next.js project using the new app directory structure can sometimes lead to unexpected issues. This guide provides step-by-step solutions to common problems encountered when using next-themes and Shadcn UI components.

The Issue

In this scenario, the dark mode toggle button registers click events and logs changes in the console, but the theme does not update across components as expected. Additionally, the theme state remains undefined in the useTheme hook.


Debugging and Fixing the Problem

1. Incorrect ThemeProvider Wrapping

Ensure the ThemeProvider from next-themes wraps your application correctly, including the {children} prop.

Here’s the corrected RootLayout implementation:

import './globals.css'
import { ThemeProvider } from '../components/theme-provider'
import ClientLayout from './components/ClientLayout'
import { cn } from '@/lib/utils'
import { Inter as FontSans } from 'next/font/google'

const fontSans = FontSans({
  subsets: ['latin'],
  variable: '--font-sans',
})

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={cn('bg-background min-h-screen font-sans antialiased', fontSans.variable)}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <ClientLayout />
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

2. Verify useTheme Hook Integration

Use the useTheme hook properly to access and update the current theme. Ensure the ThemeProvider is correctly initializing the theme context.

Here’s an updated ModeToggle component:

'use client'

import { useTheme } from 'next-themes'
import { Moon, Sun } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

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

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" 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>
  )
}

3. Tailwind CSS Configuration

Ensure your tailwind.config.js file supports dark mode. Use the class strategy for toggling themes:

module.exports = {
  darkMode: 'class',
  content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

4. Hydration Issue

If the theme state appears as undefined, it may be a hydration mismatch issue. Suppress hydration warnings and verify the theme context initialization:

<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
  {children}
</ThemeProvider>

FAQs

Why is useTheme().theme returning undefined?

This typically occurs when the ThemeProvider is not properly wrapping the application. Verify that the ThemeProvider is included at the top level of your layout and wraps all children components.

How do I disable transitions during theme changes?

Use the disableTransitionOnChange prop in the ThemeProvider to avoid unwanted animations:

<ThemeProvider disableTransitionOnChange />

How do I check the current theme?

Log the theme value from useTheme() in your component:

const { theme } = useTheme()
console.log('Current theme:', theme)

Key Takeaways

  • Wrap your entire application with the ThemeProvider.
  • Use Tailwind CSS’s darkMode: "class" setting.
  • Suppress hydration warnings for seamless rendering.
  • Test the theme state in the useTheme hook for proper initialization.

By following these steps, you can successfully implement dark mode in your Next.js project using the app directory and Shadcn UI components.