HomeCSS Tailwind
Chapter 7
07 β Dark Mode
Setup
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'media' for system preference
theme: {
extend: {},
},
plugins: [],
}class: Manual control via class="dark" on <html>
media: Automatic based on OS preference
Basic Dark Mode
<!-- Light by default, dark when .dark class is present -->
<div class="bg-white text-slate-900 dark:bg-slate-900 dark:text-white">
Content adapts to light/dark mode
</div>Common Color Pairs
<!-- Backgrounds -->
<div class="bg-white dark:bg-slate-900">...</div>
<div class="bg-slate-50 dark:bg-slate-800">...</div>
<div class="bg-slate-100 dark:bg-slate-700">...</div>
<!-- Text -->
<p class="text-slate-900 dark:text-white">...</p>
<p class="text-slate-700 dark:text-slate-300">...</p>
<p class="text-slate-600 dark:text-slate-400">...</p>
<!-- Borders -->
<div class="border border-slate-200 dark:border-slate-700">...</div>
<!-- Cards -->
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
Card content
</div>Toggle Implementation
HTML + JavaScript
<!DOCTYPE html>
<html lang="en" class="">
<head>
<script>
// Check for saved theme or system preference
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>
</head>
<body>
<button id="theme-toggle" class="p-2 rounded-lg bg-slate-200 dark:bg-slate-700">
<svg id="sun" class="w-6 h-6 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
<svg id="moon" class="w-6 h-6 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
</button>
<script>
const toggle = document.getElementById('theme-toggle');
const sun = document.getElementById('sun');
const moon = document.getElementById('moon');
toggle.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.theme = isDark ? 'dark' : 'light';
});
</script>
</body>
</html>React Hook
import { useState, useEffect } from 'react';
export function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const saved = localStorage.theme as 'light' | 'dark' | undefined;
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (saved) {
setTheme(saved);
document.documentElement.classList.toggle('dark', saved === 'dark');
} else if (systemDark) {
setTheme('dark');
document.documentElement.classList.add('dark');
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
localStorage.theme = newTheme;
};
return { theme, toggleTheme };
}
// Usage
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === 'dark' ? 'βοΈ Light' : 'π Dark'}
</button>
);
}Next.js Layout
// app/layout.tsx
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark"> {/* or remove for system preference */}
<body className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
{children}
</body>
</html>
);
}Tailwind Dark Mode Variants
<!-- All utilities support dark: variant -->
<div class="bg-white dark:bg-slate-900">
<h1 class="text-slate-900 dark:text-white">Title</h1>
<p class="text-slate-600 dark:text-slate-400">Description</p>
<button class="bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700">
Button
</button>
<input class="border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800">
</div>Custom Colors
// tailwind.config.js
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
}
}
}
}
}<div class="bg-brand-500 dark:bg-brand-900">
Brand color adapts to theme
</div>Common Component Patterns
Navigation Bar
<nav class="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
<div class="max-w-7xl mx-auto px-4">
<div class="flex justify-between h-16 items-center">
<div class="text-xl font-bold text-slate-900 dark:text-white">Logo</div>
<div class="flex items-center gap-4">
<a href="/" class="text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white">Home</a>
<a href="/about" class="text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white">About</a>
<button class="p-2 rounded-lg bg-slate-200 dark:bg-slate-700">
<span class="dark:hidden">π</span>
<span class="hidden dark:block">βοΈ</span>
</button>
</div>
</div>
</div>
</nav>Card
<div class="bg-white dark:bg-slate-800 rounded-lg shadow border border-slate-200 dark:border-slate-700 overflow-hidden">
<img src="/image.jpg" alt="Card" class="w-full h-48 object-cover">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Card Title</h3>
<p class="text-slate-600 dark:text-slate-400 mt-2">Card description</p>
<button class="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded">
Action
</button>
</div>
</div>Form
<form class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Email</label>
<input
type="email"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded focus:ring-2 focus:ring-blue-500"
>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Password</label>
<input
type="password"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded focus:ring-2 focus:ring-blue-500"
>
</div>
<button class="w-full py-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded">
Sign In
</button>
</form>Table
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-slate-50 dark:bg-slate-800">
<tr>
<th class="px-4 py-3 text-left text-slate-900 dark:text-white">Name</th>
<th class="px-4 py-3 text-left text-slate-900 dark:text-white">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700">
<td class="px-4 py-3 text-slate-900 dark:text-white">Alice</td>
<td class="px-4 py-3">
<span class="px-2 py-1 text-xs rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
Active
</span>
</td>
</tr>
</tbody>
</table>
</div>System Preference Detection
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
// Listen for changes
prefersDark.addEventListener('change', (e) => {
if (!localStorage.theme) { // Only if user hasn't set preference
document.documentElement.classList.toggle('dark', e.matches);
}
});Key Takeaways
darkMode: 'class'gives manual control;darkMode: 'media'uses system preference- Add
class="dark"to<html>to enable dark mode - Use
dark:prefix on any utility:dark:bg-slate-900,dark:text-white - Save preference in localStorage for consistency across sessions
- Common pairs: white/slate-900 backgrounds, slate-700/slate-300 text, slate-200/slate-700 borders
- Test thoroughly: Check contrast ratios and all interactive states