Tailwind v4 + Nextjs: How to Build Dark Mode with Theme Switching

npmixnpmix
108
Next.js and Tailwind CSS dark mode implementation

Implementing Dark Mode in Next.js with Tailwind CSS

Dark mode has become an essential feature for modern web applications. Not only does it reduce eye strain in low-light environments, but it also saves battery life on OLED screens and offers users the personalization they've come to expect. In this guide, I'll walk you through implementing a robust dark mode solution in your Next.js application using Tailwind CSS and the next-themes library.

Setting Up Your Project

Step 1: Install Tailwind CSS in Your Next.js Project

First, let's set up Tailwind CSS in your Next.js project following the official guidelines:

bash
# Create a new Next.js project if you don't have one
npx create-next-app@latest my-dark-mode-app --typescript --eslint --app

# Navigate to your project
cd my-dark-mode-app

# Install Tailwind CSS and its dependencies
npm install tailwindcss @tailwindcss/postcss postcss

Next, create a PostCSS configuration file in your project root:

bash
# Create postcss.config.mjs
touch postcss.config.mjs

Add the following content to your postcss.config.mjs:

javascript
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;

Finally, update your CSS file to import Tailwind:

css
/* In ./src/app/globals.css */
@import "tailwindcss";

/* Your custom styles below */

Step 2: Create a Theme Context with React

We’ll store the current theme and expose a function to update it globally.

tsx
// theme-context.tsx
'use client';
import { createContext, useEffect, useState, useContext } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextProps {
  theme: Theme;
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setThemeState] = useState<Theme>('system');

  const applyTheme = (newTheme: Theme) => {
    const root = document.documentElement;
    if (newTheme === 'dark') {
      root.setAttribute('data-theme', 'dark');
    } else if (newTheme === 'light') {
      root.setAttribute('data-theme', 'light');
    } else {
      const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      root.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
    }
  };

  useEffect(() => {
    const storedTheme = localStorage.getItem('theme') as Theme | null;
    const initialTheme = storedTheme || 'system';
    setThemeState(initialTheme);
    applyTheme(initialTheme);
  }, []);

  const setTheme = (newTheme: Theme) => {
    localStorage.setItem('theme', newTheme);
    setThemeState(newTheme);
    applyTheme(newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within a ThemeProvider');
  return context;
};

Step 3: Add a Theme Switcher Dropdown

tsx
// theme-select.tsx
'use client';
import { useTheme } from './theme-context';

export const ThemeSelect = () => {
  const { theme, setTheme } = useTheme();

  return (
    <select
      value={theme}
      onChange={(e) => setTheme(e.target.value as any)}
      className="rounded border p-2 bg-white dark:bg-black text-black dark:text-white"
    >
      <option value="light">Light</option>
      <option value="dark">Dark</option>
      <option value="system">System</option>
    </select>
  );
};

Place it anywhere in your layout (e.g., in your navbar).


Step 4: Wrap Your App with the Provider

tsx
// app/layout.tsx
import { ThemeProvider } from './theme-context';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Step 5 (Optional): Use next-themes for Simplicity

If you don’t need full control, next-themes is a great drop-in solution.

Install next-themes for Theme Management

The next-themes library makes it easy to add theme support to your Next.js application:

bash
npm install next-themes

Wrap your layout:

tsx
import { ThemeProvider } from 'next-themes';

<ThemeProvider attribute="data-theme" enableSystem defaultTheme="system">
  {children}
</ThemeProvider>

Use the hook:

tsx
import { useTheme } from 'next-themes';

const { theme, setTheme } = useTheme();

Manual vs next-themes: Quick Comparison

FeatureCustom Theme Providernext-themes
Customizability✅ Full control🚫 Limited
SSR Support⚠️ Manual✅ Built-in
Setup Speed❌ Slower✅ Fast
Bundle Size✅ No dependency🚫 Adds a package

Testing Your Implementation

To test that your dark mode is working correctly:

  1. Add some UI elements with both light and dark styles
  2. Toggle between themes using your theme toggle button
  3. Check that the system theme detection works by changing your OS theme
  4. Verify that your theme preference persists when you reload the page

Troubleshooting Common Issues

Content Flashing on Page Load

If you notice content briefly showing in the wrong theme before switching, make sure you're using the script in the head section as shown above.

Styles Not Applying

If your dark mode styles aren't applying, check:

  • That you're using the correct attribute in ThemeProvider (data-theme)
  • That your Tailwind dark mode classes are correctly prefixed with dark:
  • That you've properly imported Tailwind CSS in your global CSS file

Conclusion

Implementing dark mode in your Next.js application with Tailwind CSS v4 and next-themes provides a modern, accessible user experience. The combination of Tailwind's utility-first approach and next-themes' simple API makes it straightforward to add this essential feature to your projects.

By following this guide, you've learned how to:

  • Set up Tailwind CSS in a Next.js project
  • Implement theme switching with next-themes
  • Apply dark mode styles using Tailwind's dark: prefix
  • Handle SSR and hydration issues
  • Test and troubleshoot your implementation

Now your users can enjoy your application in whatever lighting conditions they prefer!


Have you implemented dark mode in your Next.js projects? What challenges did you face? Let me know in the comments below!

Similar articles

Never miss an update

Subscribe to receive news and special offers.

By subscribing you agree to our Privacy Policy.