Dark mode isn't a "nice to have" anymore — users expect it. Here's the fastest way to add it to a Next.js 16 app with Tailwind v4 and next-themes.
1. Install
npm install next-themes
2. Wrap your app
// app/layout.tsx
import { ThemeProvider } from 'next-themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
);
}
Key flags:
-
attribute="class"— toggles.darkclass on<html>, Tailwind picks it up -
defaultTheme="system"— respects OS preference on first visit -
suppressHydrationWarning— prevents React hydration mismatch flash
3. Configure Tailwind v4
Tailwind v4 uses CSS-based config. Add this to globals.css:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
That's it. No tailwind.config.js. No darkMode: 'class'. Tailwind v4 just works.
4. Build the toggle
// components/ThemeToggle.tsx
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <div className="w-9 h-9" />;
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
);
}
Why the mounted check? next-themes doesn't know the theme until after hydration (it reads from localStorage). Rendering before mount causes a flash of wrong theme.
5. Use in components
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<h1>Hello</h1>
</div>
Every Tailwind utility works with dark: prefix. No special components needed.
Bonus: System theme + manual override
defaultTheme="system" means:
- First visit → matches OS setting
- User clicks toggle → saves preference to localStorage
- Next visit → uses saved preference, ignores OS
This is what users expect. Don't force dark mode. Let them choose.
Full setup (dark mode + i18n + auth + dashboard): Next.js Vietnam Starter Kit — $15, ready in 5 minutes.
Top comments (0)