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

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:
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:
1# Create postcss.config.mjs
2touch postcss.config.mjs
Add the following content to your postcss.config.mjs
:
1const config = {
2 plugins: {
3 "@tailwindcss/postcss": {},
4 },
5};
6
7export default config;
Finally, update your CSS file to import Tailwind:
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.
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
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
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:
1npm install next-themes
Wrap your layout:
1import { ThemeProvider } from 'next-themes';
2
3<ThemeProvider attribute="data-theme" enableSystem defaultTheme="system">
4 {children}
5</ThemeProvider>
Use the hook:
1import { useTheme } from 'next-themes';
2
3const { theme, setTheme } = useTheme();
Manual vs next-themes
: Quick Comparison
Feature | Custom Theme Provider | next-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:
- Add some UI elements with both light and dark styles
- Toggle between themes using your theme toggle button
- Check that the system theme detection works by changing your OS theme
- 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!