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


Never miss an update
Subscribe to receive news and special offers.
By subscribing you agree to our Privacy Policy.
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.
First, let's set up Tailwind CSS in your Next.js project following the official guidelines:
# 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 postcssNext, create a PostCSS configuration file in your project root:
# Create postcss.config.mjs
touch postcss.config.mjsAdd the following content to your postcss.config.mjs:
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;Finally, update your CSS file to import Tailwind:
/* In ./src/app/globals.css */
@import "tailwindcss";
/* Your custom styles below */We’ll store the current theme and expose a function to update it globally.
// 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;
};// 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).
// 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>
);
}next-themes for SimplicityIf 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:
npm install next-themesWrap your layout:
import { ThemeProvider } from 'next-themes';
<ThemeProvider attribute="data-theme" enableSystem defaultTheme="system">
{children}
</ThemeProvider>Use the hook:
import { useTheme } from 'next-themes';
const { theme, setTheme } = useTheme();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 |
To test that your dark mode is working correctly:
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.
If your dark mode styles aren't applying, check:
data-theme)dark: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:
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!