Dark mode has become essential for modern applications—it reduces eye strain in low-light environments and saves battery on OLED screens. Tailwind makes implementing dark mode incredibly easy with dark variant utilities. In this lesson, you'll learn to implement dark mode using class-based strategy, create theme toggles, and build interfaces that look beautiful in both light and dark themes.
Dark Mode Strategies
Tailwind v4 supports dark mode out of the box with class-based strategy as the default:
Class-Based Dark Mode (Default in v4)
Dark mode is activated by adding a dark class to the <html> element:
- Light mode:
<html> - Dark mode:
<html class="dark">
Using Dark Mode Utilities
<!-- Element that changes in dark mode -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white p-6 rounded-lg">
<!-- Light mode: white background, dark text
Dark mode: dark gray background, white text -->
This element adapts to dark mode
</div>
<!-- Button with dark mode styles -->
<button class="bg-blue-500 dark:bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700">
Click Me
</button>
<!-- Card with dark mode -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-lg">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
Card Title
</h3>
<p class="text-gray-600 dark:text-gray-300">
Card content that adapts to theme
</p>
</div>🌙 Dark Mode Best Practice
Always pair dark mode utilities with their light mode counterparts. For example: bg-white dark:bg-gray-900 ensures you explicitly define both states.
Implementing a Dark Mode Toggle
Create a simple theme toggle that adds/removes the dark class:
Basic JavaScript Toggle
<!-- Theme toggle button -->
<button
id="theme-toggle"
class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
aria-label="Toggle dark mode"
>
<!-- Sun icon (visible in dark mode) -->
<svg class="hidden dark:block w-6 h-6" 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 (visible in light mode) -->
<svg class="block dark:hidden w-6 h-6" 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>
<script>
const themeToggle = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
// Check for saved theme preference or default to light mode
const currentTheme = localStorage.getItem('theme') || 'light';
// Apply the current theme
if (currentTheme === 'dark') {
htmlElement.classList.add('dark');
}
// Toggle theme on button click
themeToggle.addEventListener('click', () => {
htmlElement.classList.toggle('dark');
// Save preference to localStorage
const theme = htmlElement.classList.contains('dark') ? 'dark' : 'light';
localStorage.setItem('theme', theme);
});
</script>React/Next.js Implementation
'use client';
import { useEffect, useState } from 'react';
export default function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
// Initialize theme from localStorage
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const shouldBeDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
setIsDark(shouldBeDark);
if (shouldBeDark) {
document.documentElement.classList.add('dark');
}
}, []);
// Toggle theme
const toggleTheme = () => {
const newIsDark = !isDark;
setIsDark(newIsDark);
if (newIsDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
};
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 transition-colors"
aria-label="Toggle dark mode"
>
{isDark ? (
// Sun icon
<svg className="w-6 h-6" 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
<svg className="w-6 h-6" 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>
);
}Preventing Flash of Unstyled Content
Add this script to your <head> to prevent the flash of wrong theme on page load:
<!-- Add this BEFORE any other scripts -->
<script>
// Check for dark mode preference at the earliest possible moment
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>Dark Mode Color Strategies
Choosing the right colors for dark mode is crucial for readability and aesthetics:
Background Colors
<!-- Pure white/black (high contrast - can be harsh) -->
<div class="bg-white dark:bg-black">
High contrast backgrounds
</div>
<!-- Soft backgrounds (recommended) -->
<div class="bg-white dark:bg-gray-900">
Body background
</div>
<div class="bg-gray-50 dark:bg-gray-800">
Card/section background
</div>
<div class="bg-gray-100 dark:bg-gray-700">
Subtle highlight/hover state
</div>
<!-- Colored backgrounds -->
<div class="bg-blue-50 dark:bg-blue-950">
Blue tinted background
</div>
<div class="bg-purple-50 dark:bg-purple-950">
Purple tinted background
</div>Text Colors
<!-- Primary text (highest contrast) -->
<p class="text-gray-900 dark:text-white">
Main content text
</p>
<!-- Secondary text (medium contrast) -->
<p class="text-gray-700 dark:text-gray-200">
Secondary content
</p>
<!-- Tertiary text (lower contrast) -->
<p class="text-gray-600 dark:text-gray-300">
Metadata, captions
</p>
<!-- Muted text (lowest contrast) -->
<p class="text-gray-500 dark:text-gray-400">
Disabled or placeholder text
</p>
<!-- Colored text -->
<p class="text-blue-600 dark:text-blue-400">
Links and accents
</p>
<p class="text-green-600 dark:text-green-400">
Success messages
</p>
<p class="text-red-600 dark:text-red-400">
Error messages
</p>Border Colors
<!-- Subtle borders -->
<div class="border border-gray-200 dark:border-gray-700">
Very subtle separation
</div>
<!-- Medium borders -->
<div class="border border-gray-300 dark:border-gray-600">
Clear separation
</div>
<!-- Strong borders -->
<div class="border-2 border-gray-400 dark:border-gray-500">
Emphasized border
</div>
<!-- Colored borders -->
<div class="border-2 border-blue-500 dark:border-blue-400">
Accent border
</div>Dark Mode Color Contrast
- Avoid pure black (#000): Use gray-900 instead for less eye strain
- Reduce contrast slightly: Pure white on pure black is harsh
- Lighten accent colors: Bright colors need to be lighter in dark mode
- Test readability: Ensure sufficient contrast ratios (WCAG)
Dark Mode Component Examples
Example 1: Navigation Bar
<nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo -->
<div class="flex items-center">
<img
src="/logo-light.svg"
alt="Logo"
class="h-8 dark:hidden"
>
<img
src="/logo-dark.svg"
alt="Logo"
class="h-8 hidden dark:block"
>
</div>
<!-- Navigation links -->
<div class="hidden md:flex gap-6">
<a href="#" class="text-gray-700 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Home
</a>
<a href="#" class="text-gray-700 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
About
</a>
<a href="#" class="text-gray-700 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Contact
</a>
</div>
<!-- Theme toggle -->
<button class="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200">
Toggle Theme
</button>
</div>
</div>
</nav>Example 2: Card Component
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg dark:shadow-gray-900/50 overflow-hidden transition-colors">
<!-- Image -->
<img
src="image.jpg"
alt="Card"
class="w-full h-48 object-cover"
>
<!-- Content -->
<div class="p-6">
<!-- Category badge -->
<span class="inline-block px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs font-semibold rounded-full mb-3">
Technology
</span>
<!-- Title -->
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
Card Title
</h3>
<!-- Description -->
<p class="text-gray-600 dark:text-gray-300 mb-4">
This is a description that adapts to both light and dark themes seamlessly.
</p>
<!-- Footer -->
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-2">
<img
src="avatar.jpg"
alt="Author"
class="w-8 h-8 rounded-full"
>
<span class="text-sm text-gray-700 dark:text-gray-300">
John Doe
</span>
</div>
<button class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-semibold text-sm">
Read More →
</button>
</div>
</div>
</div>Example 3: Form Elements
<form class="space-y-4">
<!-- Input field -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Address
</label>
<input
type="email"
class="w-full px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent outline-none transition-colors"
placeholder="you@example.com"
>
</div>
<!-- Textarea -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Message
</label>
<textarea
rows="4"
class="w-full px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent outline-none transition-colors resize-none"
placeholder="Your message..."
></textarea>
</div>
<!-- Checkbox -->
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="w-4 h-4 text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
>
<span class="text-sm text-gray-700 dark:text-gray-300">
Subscribe to newsletter
</span>
</label>
<!-- Submit button -->
<button class="w-full bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 text-white font-semibold py-3 rounded-lg transition-colors">
Submit
</button>
</form>Example 4: Alert Messages
<!-- Success alert -->
<div class="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-500 dark:border-green-400 p-4 rounded-r-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-green-500 dark:text-green-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<div>
<h3 class="text-sm font-semibold text-green-800 dark:text-green-200">Success!</h3>
<p class="text-sm text-green-700 dark:text-green-300 mt-1">Your changes have been saved.</p>
</div>
</div>
</div>
<!-- Error alert -->
<div class="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 dark:border-red-400 p-4 rounded-r-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-red-500 dark:text-red-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<div>
<h3 class="text-sm font-semibold text-red-800 dark:text-red-200">Error!</h3>
<p class="text-sm text-red-700 dark:text-red-300 mt-1">Something went wrong. Please try again.</p>
</div>
</div>
</div>
<!-- Info alert -->
<div class="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500 dark:border-blue-400 p-4 rounded-r-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-blue-500 dark:text-blue-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<div>
<h3 class="text-sm font-semibold text-blue-800 dark:text-blue-200">Information</h3>
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">New updates are available.</p>
</div>
</div>
</div>Example 5: Dashboard Layout
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
<!-- Header -->
<header class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="px-6 py-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Dashboard
</h1>
</div>
</header>
<!-- Main content -->
<main class="p-6">
<!-- Stats grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Stat card -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Total Users</p>
<p class="text-3xl font-bold text-gray-900 dark:text-white">1,234</p>
<p class="text-sm text-green-600 dark:text-green-400 mt-2">↑ 12% from last month</p>
</div>
<!-- More stat cards... -->
</div>
<!-- Content area -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
Recent Activity
</h2>
<div class="space-y-4">
<div class="flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700 last:border-0">
<div>
<p class="font-semibold text-gray-900 dark:text-white">New user signup</p>
<p class="text-sm text-gray-600 dark:text-gray-400">john@example.com</p>
</div>
<span class="text-sm text-gray-500 dark:text-gray-400">2 min ago</span>
</div>
<!-- More activity items... -->
</div>
</div>
</main>
</div>Images and Icons in Dark Mode
Handle images and icons that need to adapt to dark mode:
SVG Icons with Dark Mode
<!-- Icon that changes color -->
<svg class="w-6 h-6 text-gray-700 dark:text-gray-300" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6z"/>
</svg>
<!-- Different icons for light/dark mode -->
<svg class="w-6 h-6 dark:hidden" fill="currentColor" viewBox="0 0 20 20">
<!-- Light mode icon -->
</svg>
<svg class="w-6 h-6 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
<!-- Dark mode icon -->
</svg>Logo Variations
<!-- Show different logo for each theme -->
<img
src="/logo-light.svg"
alt="Logo"
class="h-8 dark:hidden"
>
<img
src="/logo-dark.svg"
alt="Logo"
class="h-8 hidden dark:block"
>Image Opacity
<!-- Reduce image brightness in dark mode -->
<img
src="photo.jpg"
alt="Photo"
class="w-full rounded-lg opacity-100 dark:opacity-80"
>
<!-- Add overlay to images in dark mode -->
<div class="relative">
<img src="photo.jpg" alt="Photo" class="w-full rounded-lg">
<div class="absolute inset-0 bg-black/0 dark:bg-black/20 rounded-lg"></div>
</div>Interactive Dark Mode Explorer
Experiment with dark mode utilities:
Dark Mode Color Explorer
See how colors change between light and dark themes
Utilities
Current Classes:
p-8 rounded-lgPreview
Dark Mode Playground
Practice building dark mode components:
Build Dark Mode UIs
Create components that look great in both themes
Dark Mode Demo
Toggle your system's dark mode to see the changes
Sample Card
This card adapts to both light and dark themes. Notice how the colors, borders, and shadows all change smoothly.
Dark Mode Best Practices
Follow These Guidelines:
- Test both themes: Always check components in light and dark mode
- Sufficient contrast: Ensure WCAG compliance in both themes
- Avoid pure black: Use gray-900 instead of #000 for less eye strain
- Lighten accent colors: Bright colors need adjustment in dark mode
- Save user preference: Remember theme choice in localStorage
- Respect system preference: Default to OS dark mode setting
- Prevent flash: Load theme before page renders
- Smooth transitions: Add transition-colors for theme switches
- Test images: Ensure images work in both themes
Common Dark Mode Color Patterns
Recommended Color Combinations:
- Page background:
bg-white dark:bg-gray-900 - Card background:
bg-white dark:bg-gray-800 - Subtle background:
bg-gray-50 dark:bg-gray-800 - Primary text:
text-gray-900 dark:text-white - Secondary text:
text-gray-600 dark:text-gray-300 - Borders:
border-gray-200 dark:border-gray-700 - Links:
text-blue-600 dark:text-blue-400
Key Takeaways
- Tailwind v4 uses class-based dark mode by default
- Add
darkclass to<html>to enable dark mode - Use
dark:prefix to apply styles in dark mode only - Avoid pure black—use gray-900 for better readability
- Lighten accent colors in dark mode for better visibility
- Save theme preference to localStorage
- Prevent flash by applying theme class before page renders
- Provide theme toggle for user control
- Test contrast ratios in both themes for accessibility
- Use
transition-colorsfor smooth theme switching
What's Next?
Congratulations! You've completed the Responsive Design category. You can now build responsive interfaces that work on all devices and support both light and dark themes.
You've now covered the core fundamentals of Tailwind CSS! In future tutorials, you'll learn advanced topics like transforms, transitions, animations, and best practices for production applications.
🎯 Practice Challenge!
Build a complete dashboard interface with full dark mode support! Include a navigation bar, sidebar, stat cards, data tables, and forms. Make sure everything looks great in both light and dark themes. Add a theme toggle button that saves the preference. Test the color contrast in both modes and ensure all interactive elements have proper focus states!