Back to Blog
Lesson 35 of the Next.js: Build Full-Stack Apps with the App Router course
Next.jsAugust 22, 20264 min read

Implementing Dark Mode in Next.js with Tailwind CSS

Learn how to implement dark mode in your Next.js app using next-themes and Tailwind. Persist user preferences and ensure a flicker-free UI.

Next.jsTailwinddark modeUIcomponents
Close-up of colorful CSS code lines on a computer screen for web development.

Previously in this course, we explored Middleware Basics to intercept and handle requests. In this lesson, we add the final touch to our UI: implementing dark mode to give our users a comfortable reading experience that respects their system settings.

The Challenge of Theming in Next.js

When building with the App Router, you'll find that simply toggling a class on the <body> element isn't enough. Because Next.js renders content on the server, the server doesn't know if the user prefers "dark" or "light" mode until the JavaScript executes on the client. This often results in a "flash of incorrect theme" (FOUC), where the page renders in light mode for a split second before snapping to dark mode.

To solve this, we use next-themes, an abstraction that handles:

  1. System Preferences: Respecting the user's OS settings automatically.
  2. Persistence: Saving the user's choice in localStorage.
  3. Hydration Safety: Ensuring the theme is applied before the first paint to prevent flashes.

Step 1: Install and Configure

First, install the library:

Bash
npm install next-themes

Next, we need a wrapper to provide the theme context to our application. Because this requires React state and context, it must be a Client Component. Create components/theme-provider.tsx:

TSX
CE9178">'use client';

import { ThemeProvider as NextThemesProvider } from CE9178">'next-themes';

export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

Now, wrap your application in the app/layout.tsx file:

TSX
import { ThemeProvider } from CE9178">'@/components/theme-provider';

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

Note: suppressHydrationWarning on the <html> tag is required because next-themes updates the class attribute on the server/client mismatch during the first render.

Step 2: Enabling Tailwind Support

Ensure your tailwind.config.js is set to use the class strategy. This tells Tailwind to look for a dark class on the parent element to trigger styles, rather than relying on the CSS media query alone.

JAVASCRIPT
/** @type {import(CE9178">'tailwindcss').Config} */
module.exports = {
  darkMode: CE9178">'class', // <--- Ensure this is set
  // ... rest of config
}

Now you can use the dark: prefix in your utility classes as discussed in our Advanced Tailwind Configurations lesson:

TSX
<div className="bg-white dark:bg-slate-900 text-black dark:text-white">
  <h1>Hello World</h1>
</div>

Step 3: Building the Toggle

Finally, create a simple button to switch themes. This component uses the useTheme hook provided by next-themes.

TSX
CE9178">'use client';

import { useTheme } from CE9178">'next-themes';
import { useEffect, useState } from CE9178">'react';

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

  // Avoid hydration mismatch
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

  return (
    <button onClick={() => setTheme(theme === CE9178">'dark' ? CE9178">'light' : CE9178">'dark')}>
      Toggle {theme === CE9178">'dark' ? CE9178">'Light' : CE9178">'Dark'} Mode
    </button>
  );
}

Common Pitfalls

  • Hydration Mismatch: If you try to access the theme object directly without the mounted check, React will throw a warning because the server-rendered HTML doesn't match the client's initial render.
  • Missing darkMode: 'class': If your styles aren't changing, double-check your tailwind.config.js. Without this setting, Tailwind will ignore the dark: classes.
  • Forgetting suppressHydrationWarning: Without this attribute on the <html> tag, React will flag the class attribute change as an error.

Practice Exercise

Add the ThemeToggle component to your navigation bar (created in our earlier lessons on Implementing Navigation with Link). Verify that it remembers your choice after refreshing the page.

FAQ

Q: Does this work with Server Components? A: The toggle button must be a Client Component, but your layout remains a Server Component. The next-themes provider effectively manages the transition.

Q: Can I use this for more than just dark mode? A: Yes, next-themes supports any arbitrary theme names (e.g., 'light', 'dark', 'midnight'), provided you define the corresponding CSS classes.

Recap

We've successfully added a robust, flicker-free dark mode to our application. By combining the class strategy in Tailwind with next-themes, we've ensured our blog provides a high-quality, accessible UI that persists across user sessions.

Up next: We'll dive into Advanced Metadata Patterns to ensure our blog posts are perfectly optimized for search engines and social sharing.

Similar Posts