Tailwind CSS Dark Mode: Complete Implementation Guide
Kamis, 16 Jan 2025
Dark mode has become an essential feature for modern web applications. Tailwind CSS makes implementing dark mode straightforward with built-in utilities and flexible configuration options. This guide covers everything you need to know about Tailwind CSS dark mode implementation.
Understanding Tailwind Dark Mode Strategies
Tailwind CSS offers two strategies for dark mode: media and class. Each approach has distinct use cases and trade-offs.
Media Strategy (Default)
The media strategy uses the prefers-color-scheme CSS media query to automatically detect the user’s system preference:
// tailwind.config.js
module.exports = {
darkMode: 'media',
// ...
}
With this configuration, dark mode activates automatically when the user’s operating system is set to dark mode. No JavaScript is required.
Class Strategy
The class strategy gives you manual control over dark mode by toggling a dark class on the HTML element:
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
This approach is ideal when you want users to override their system preference or when building applications that need programmatic theme control.
Using the dark: Variant
Once configured, apply dark mode styles using the dark: variant prefix:
<div class="bg-white dark:bg-gray-900">
<h1 class="text-gray-900 dark:text-white">
Welcome to Dark Mode
</h1>
<p class="text-gray-600 dark:text-gray-300">
This text adapts to the current theme.
</p>
</div>
The dark: variant works with any Tailwind utility class, making it simple to define both light and dark styles inline.
Combining with Other Variants
Dark mode variants can be combined with responsive breakpoints and state variants:
<button class="
bg-blue-500 hover:bg-blue-600
dark:bg-blue-600 dark:hover:bg-blue-700
md:px-6 md:py-3
">
Click Me
</button>
System Preference Detection
For the class strategy, you’ll want to detect the user’s system preference on page load. Add this script to your HTML <head> to prevent flash of unstyled content (FOUC):
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
This script runs before the page renders, checking localStorage first, then falling back to the system preference.
Manual Toggle with localStorage Persistence
Create a theme toggle that remembers user preferences across sessions:
// theme-toggle.js
function toggleTheme() {
const html = document.documentElement;
if (html.classList.contains('dark')) {
html.classList.remove('dark');
localStorage.theme = 'light';
} else {
html.classList.add('dark');
localStorage.theme = 'dark';
}
}
function setTheme(theme) {
const html = document.documentElement;
if (theme === 'dark') {
html.classList.add('dark');
localStorage.theme = 'dark';
} else if (theme === 'light') {
html.classList.remove('dark');
localStorage.theme = 'light';
} else {
// System preference
localStorage.removeItem('theme');
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
}
}
Toggle Button Component
Here’s a simple toggle button implementation:
<button
onclick="toggleTheme()"
class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700"
aria-label="Toggle dark mode"
>
<!-- Sun icon (shown in dark mode) -->
<svg class="hidden dark:block w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
</svg>
<!-- Moon icon (shown in light mode) -->
<svg class="block dark:hidden w-5 h-5 text-gray-700" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
</svg>
</button>
React and Next.js Integration with next-themes
For React and Next.js applications, the next-themes library provides an elegant solution with built-in SSR support:
npm install next-themes
Setting Up the Theme Provider
Wrap your application with the ThemeProvider:
// app/providers.jsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// app/layout.jsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
Creating a Theme Toggle Component
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <div className="w-9 h-9" /> // Placeholder to prevent layout shift
}
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700
hover:bg-gray-300 dark:hover:bg-gray-600
transition-colors"
aria-label="Toggle theme"
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
)
}
Theme Selector with System Option
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeSelector() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value)}
className="px-3 py-2 rounded-lg border border-gray-300
dark:border-gray-600 bg-white dark:bg-gray-800
text-gray-900 dark:text-white"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
)
}
Best Practices for Dark Mode
1. Design with Both Modes in Mind
Plan your color palette for both light and dark modes from the start. Use Tailwind’s color scale consistently:
<!-- Good: Consistent color relationships -->
<div class="bg-gray-50 dark:bg-gray-900">
<p class="text-gray-700 dark:text-gray-300">Content</p>
</div>
<!-- Avoid: Inconsistent jumps in color scale -->
<div class="bg-white dark:bg-black">
<p class="text-gray-900 dark:text-gray-100">Content</p>
</div>
2. Use CSS Custom Properties for Complex Themes
For applications with multiple themes or complex color requirements, combine Tailwind with CSS custom properties:
/* globals.css */
:root {
--color-primary: 59 130 246;
--color-background: 255 255 255;
--color-text: 17 24 39;
}
.dark {
--color-primary: 96 165 250;
--color-background: 17 24 39;
--color-text: 243 244 246;
}
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: 'rgb(var(--color-primary) / <alpha-value>)',
background: 'rgb(var(--color-background) / <alpha-value>)',
text: 'rgb(var(--color-text) / <alpha-value>)',
},
},
},
}
3. Handle Images and Media
Adjust images for dark mode using opacity or filters:
<!-- Reduce brightness in dark mode -->
<img
src="/hero.jpg"
class="dark:brightness-90 dark:contrast-105"
alt="Hero image"
/>
<!-- Show different images per theme -->
<img
src="/logo-light.svg"
class="dark:hidden"
alt="Logo"
/>
<img
src="/logo-dark.svg"
class="hidden dark:block"
alt="Logo"
/>
4. Consider Accessibility
Maintain sufficient contrast ratios in both modes. Use Tailwind’s opacity modifiers to fine-tune:
<p class="text-gray-600 dark:text-gray-400">
<!-- May have low contrast in dark mode -->
</p>
<p class="text-gray-600 dark:text-gray-300">
<!-- Better contrast in dark mode -->
</p>
5. Prevent Flash of Incorrect Theme
Always include the theme detection script in the <head> before any stylesheets load. For Next.js with next-themes, add suppressHydrationWarning to the <html> element.
Common Patterns
Card Component
<div class="
bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
rounded-xl shadow-sm dark:shadow-gray-900/20
p-6
">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Card Title
</h3>
<p class="mt-2 text-gray-600 dark:text-gray-400">
Card description goes here.
</p>
</div>
Input Field
<input
type="text"
placeholder="Enter text..."
class="
w-full px-4 py-2 rounded-lg
bg-white dark:bg-gray-800
border border-gray-300 dark:border-gray-600
text-gray-900 dark:text-white
placeholder-gray-500 dark:placeholder-gray-400
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:focus:ring-blue-400
"
/>
Navigation Bar
<nav class="
bg-white dark:bg-gray-900
border-b border-gray-200 dark:border-gray-800
sticky top-0 z-50
">
<div class="max-w-7xl mx-auto px-4 py-4">
<a href="/" class="text-gray-900 dark:text-white font-bold">
Logo
</a>
</div>
</nav>
Conclusion
Tailwind CSS provides a robust foundation for implementing dark mode in your applications. Whether you choose the automatic media strategy or the flexible class strategy, Tailwind’s utility-first approach makes it easy to maintain consistent styling across both themes.
Start with the class strategy if you need user controls, integrate next-themes for React applications, and follow the best practices outlined above to create a polished dark mode experience that your users will appreciate.