Dark Mode
Implement dark mode with Tailwind's dark: variant and next-themes
What you'll learn
Apply dark: variants to any utility class
Choose between class-based and media-based dark mode strategy
Integrate next-themes for theme toggling in Next.js
Build a cohesive dark mode color palette
Handle images and media for dark mode
Dark Variant Basics
Tailwind v4 uses @custom-variant dark (&:is(.dark *)) — add .dark class to parent, use dark: prefixes.
<div
class="rounded-lg bg-white p-6 text-gray-900 dark:bg-gray-900 dark:text-gray-100"
>
<h2 class="text-lg font-semibold dark:text-white">Card Title</h2>
<p class="text-gray-600 dark:text-gray-400">Content adapts to both themes.</p>
</div>
Strategy: Class vs Media
Toggle via .dark class on <html>. User controls the theme.
<button onclick="document.documentElement.classList.toggle('dark')">Toggle</button>
next-themes Integration
// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// Any component
import { useTheme } from "next-themes"
function Toggle() {
const { theme, setTheme } = useTheme()
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
>
Toggle
</button>
)
}
Color Palette for Dark Mode
Map every surface color with its dark counterpart.
<div class="rounded-xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h3 class="text-gray-900 dark:text-white">Primary</h3>
<p class="text-gray-600 dark:text-gray-400">Secondary text</p>
</div>
<input
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-gray-900 placeholder-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
placeholder="Search..."
/>
Tip
Use subtle grays (gray-800, gray-900) for dark surfaces, not pure black. Pure black creates harsh contrast against other dark elements.
Images in Dark Mode
<img
src="photo.jpg"
alt=""
class="rounded-lg dark:opacity-80 dark:brightness-90"
/>
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)" />
<img src="logo-light.svg" alt="Logo" class="h-8" />
</picture>
Gotcha
Avoid flash of wrong theme. next-themes with enableSystem injects a script before React hydrates to prevent FOIT.
Dark Mode Toggle with Theme-Aware Cards
Code
tsx
1<div>2<button class="...">Toggle</button>3<div class="..."><h3>Card Title</h3><p>Content</p></div>4</div>Key Takeaways
Use dark: variant on every color utility for theme-aware components
Class-based strategy gives users control; media matches OS only
next-themes (ThemeProvider attribute="class") handles seamless toggling
Subtle grays for dark surfaces, not pure black
Dim images with dark:opacity-80 dark:brightness-90