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

AndryAndry Dina
96
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
1# Create a new Next.js project if you don't have one
2npx create-next-app@latest my-dark-mode-app --typescript --eslint --app
3
4# Navigate to your project
5cd my-dark-mode-app
6
7# Install Tailwind CSS and its dependencies
8npm install tailwindcss @tailwindcss/postcss postcss

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

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

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

javascript
1const config = {
2  plugins: {
3    "@tailwindcss/postcss": {},
4  },
5};
6
7export default config;

Finally, update your CSS file to import Tailwind:

css
1/* In ./src/app/globals.css */
2@import "tailwindcss";
3
4/* 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
1// theme-context.tsx
2'use client';
3import { createContext, useEffect, useState, useContext } from 'react';
4
5type Theme = 'light' | 'dark' | 'system';
6
7interface ThemeContextProps {
8  theme: Theme;
9  setTheme: (theme: Theme) => void;
10}
11
12const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
13
14export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
15  const [theme, setThemeState] = useState<Theme>('system');
16
17  const applyTheme = (newTheme: Theme) => {
18    const root = document.documentElement;
19    if (newTheme === 'dark') {
20      root.setAttribute('data-theme', 'dark');
21    } else if (newTheme === 'light') {
22      root.setAttribute('data-theme', 'light');
23    } else {
24      const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
25      root.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
26    }
27  };
28
29  useEffect(() => {
30    const storedTheme = localStorage.getItem('theme') as Theme | null;
31    const initialTheme = storedTheme || 'system';
32    setThemeState(initialTheme);
33    applyTheme(initialTheme);
34  }, []);
35
36  const setTheme = (newTheme: Theme) => {
37    localStorage.setItem('theme', newTheme);
38    setThemeState(newTheme);
39    applyTheme(newTheme);
40  };
41
42  return (
43    <ThemeContext.Provider value={{ theme, setTheme }}>
44      {children}
45    </ThemeContext.Provider>
46  );
47};
48
49export const useTheme = () => {
50  const context = useContext(ThemeContext);
51  if (!context) throw new Error('useTheme must be used within a ThemeProvider');
52  return context;
53};

Step 3: Add a Theme Switcher Dropdown

tsx
1// theme-select.tsx
2'use client';
3import { useTheme } from './theme-context';
4
5export const ThemeSelect = () => {
6  const { theme, setTheme } = useTheme();
7
8  return (
9    <select
10      value={theme}
11      onChange={(e) => setTheme(e.target.value as any)}
12      className="rounded border p-2 bg-white dark:bg-black text-black dark:text-white"
13    >
14      <option value="light">Light</option>
15      <option value="dark">Dark</option>
16      <option value="system">System</option>
17    </select>
18  );
19};

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


Step 4: Wrap Your App with the Provider

tsx
1// app/layout.tsx
2import { ThemeProvider } from './theme-context';
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5  return (
6    <html lang="en">
7      <body>
8        <ThemeProvider>
9          {children}
10        </ThemeProvider>
11      </body>
12    </html>
13  );
14}

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
1npm install next-themes

Wrap your layout:

tsx
1import { ThemeProvider } from 'next-themes';
2
3<ThemeProvider attribute="data-theme" enableSystem defaultTheme="system">
4  {children}
5</ThemeProvider>

Use the hook:

tsx
1import { useTheme } from 'next-themes';
2
3const { 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.