Dark Mode
Dark Mode
Dark mode has become an essential feature in modern web applications. Users expect the ability to switch between light and dark themes, and Tailwind CSS makes implementing dark mode remarkably simple with its built-in dark: variant.
In this lesson, we'll explore how to implement dark mode, configure different strategies, design effectively for both themes, and create seamless dark mode experiences.
The dark: Variant
Tailwind provides a dark: variant that applies styles only when dark mode is active:
Basic Dark Mode Usage
<!-- Light mode: white background, dark text -->
<!-- Dark mode: dark background, light text -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
This text adapts to light and dark mode
</div>
<!-- Cards with dark mode support -->
<div class="bg-white dark:bg-gray-800 shadow-lg dark:shadow-gray-900/50
rounded-lg p-6 border border-gray-200 dark:border-gray-700">
<h3 class="text-gray-900 dark:text-white font-bold text-xl mb-2">
Card Title
</h3>
<p class="text-gray-600 dark:text-gray-300">
Card content that looks good in both themes
</p>
</div>
<!-- Buttons with dark mode variants -->
<button class="px-6 py-3 bg-blue-500 dark:bg-blue-600
hover:bg-blue-600 dark:hover:bg-blue-700
text-white rounded-lg shadow-md dark:shadow-lg">
Click Me
</button>
Dark Mode Strategies
Tailwind supports two strategies for enabling dark mode: media and class. You configure this in your tailwind.config.js:
1. Media Strategy (System Preference)
This strategy uses the user's operating system preference:
Media Strategy Configuration
// tailwind.config.js
module.exports = {
darkMode: 'media', // Uses prefers-color-scheme media query
theme: {
// ... your theme config
}
}
With media strategy, dark mode is automatically enabled when the user has dark mode enabled in their OS settings. You don't need any JavaScript—it's purely CSS-based.
- Simple sites without user accounts
- When you want automatic system integration
- Static sites or documentation
- When you don't need manual toggle functionality
2. Class Strategy (Manual Toggle)
This strategy looks for a dark class on the <html> or <body> element:
Class Strategy Configuration
// tailwind.config.js
module.exports = {
darkMode: 'class', // Looks for 'dark' class in DOM
theme: {
// ... your theme config
}
}
With class strategy, you control dark mode via JavaScript by adding/removing the dark class:
HTML Structure for Class Strategy
<!-- Light mode -->
<html>
<!-- content -->
</html>
<!-- Dark mode enabled -->
<html class="dark">
<!-- content -->
</html>
Implementing Dark Mode Toggle with JavaScript
Let's build a complete dark mode toggle with persistence:
Complete Dark Mode Toggle Implementation
<!-- Dark mode toggle button -->
<button id="theme-toggle" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700
hover:bg-gray-300 dark:hover:bg-gray-600
transition-colors duration-200">
<!-- Sun icon (shown in dark mode) -->
<svg class="w-6 h-6 text-yellow-500 hidden dark:block"
fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" 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" clip-rule="evenodd"></path>
</svg>
<!-- Moon icon (shown in light mode) -->
<svg class="w-6 h-6 text-gray-700 block dark:hidden"
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"></path>
</svg>
</button>
<script>
// Dark mode toggle logic
const themeToggle = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
// Check for saved theme preference or default to system preference
const getTheme = () => {
// Check localStorage first
if (localStorage.theme === 'dark') {
return 'dark';
}
if (localStorage.theme === 'light') {
return 'light';
}
// Fall back to system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
// Apply theme
const applyTheme = (theme) => {
if (theme === 'dark') {
htmlElement.classList.add('dark');
} else {
htmlElement.classList.remove('dark');
}
};
// Initialize theme on page load
applyTheme(getTheme());
// Toggle theme on button click
themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.classList.contains('dark') ? 'dark' : 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
localStorage.theme = newTheme;
});
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
// Only auto-switch if user hasn't set a preference
if (!localStorage.theme) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
</script>
Designing for Dark Mode
Effective dark mode isn't just about inverting colors—it requires thoughtful design decisions:
1. Color Considerations
Dark Mode Color Best Practices
<!-- DON'T use pure black (#000000) -->
<div class="dark:bg-black">Too harsh!</div>
<!-- DO use dark grays for backgrounds -->
<div class="dark:bg-gray-900">Better for eyes</div>
<!-- DON'T use pure white text on dark -->
<p class="dark:text-white">Too much contrast!</p>
<!-- DO use slightly muted text colors -->
<p class="dark:text-gray-100">More comfortable</p>
<!-- Adjust colors for dark mode -->
<div class="bg-blue-500 dark:bg-blue-600">
<!-- Slightly brighter blues work better in dark mode -->
</div>
<!-- Reduce color saturation in dark mode -->
<button class="bg-green-500 dark:bg-green-600
text-white hover:bg-green-600 dark:hover:bg-green-700">
Success Button
</button>
2. Elevation and Shadows
Shadows in Dark Mode
<!-- Light mode shadows -->
<div class="shadow-lg dark:shadow-gray-900/50">
<!-- Darker shadows for dark mode -->
</div>
<!-- Use borders for separation instead of shadows -->
<div class="shadow-md dark:shadow-none
dark:border dark:border-gray-700">
Elevated card
</div>
<!-- Lighter backgrounds for elevation in dark mode -->
<div class="bg-white dark:bg-gray-800">
<!-- Nested elevated element -->
<div class="bg-gray-50 dark:bg-gray-700 p-4">
Higher elevation = lighter in dark mode
</div>
</div>
3. Border and Divider Colors
Borders in Dark Mode
<!-- Subtle borders that work in both modes -->
<div class="border border-gray-200 dark:border-gray-700">
Content
</div>
<!-- Dividers -->
<hr class="border-gray-200 dark:border-gray-700" />
<!-- Table borders -->
<table class="border-collapse">
<tr class="border-b border-gray-200 dark:border-gray-700">
<td class="border-r border-gray-200 dark:border-gray-700">Cell</td>
</tr>
</table>
Images in Dark Mode
Images need special attention in dark mode:
Handling Images in Dark Mode
<!-- Reduce brightness of images in dark mode -->
<img src="photo.jpg" class="dark:opacity-80" alt="Photo" />
<!-- Different images for different modes -->
<img src="logo-light.png" class="block dark:hidden" alt="Logo" />
<img src="logo-dark.png" class="hidden dark:block" alt="Logo" />
<!-- Add subtle background to transparent images -->
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg">
<img src="transparent-icon.png" alt="Icon" />
</div>
<!-- Invert icons that should be light in dark mode -->
<img src="icon.svg" class="dark:invert" alt="Icon" />
<!-- Ring to highlight images in dark mode -->
<img src="avatar.jpg"
class="rounded-full ring-2 ring-gray-200 dark:ring-gray-700"
alt="Avatar" />
System Preference Detection
You can detect and respond to the user's system color scheme preference:
Detecting System Preference
<script>
// Check if user prefers dark mode
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
console.log('User prefers dark mode:', prefersDark.matches);
// Listen for changes in system preference
prefersDark.addEventListener('change', (e) => {
console.log('System theme changed to:', e.matches ? 'dark' : 'light');
// Only apply if user hasn't set manual preference
if (!localStorage.getItem('theme')) {
applySystemTheme(e.matches);
}
});
function applySystemTheme(isDark) {
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
</script>
Complete Dark Mode Example
Here's a comprehensive dark mode implementation for a web application:
Full Dark Mode Application
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dark Mode App</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Initialize theme before page renders (prevents flash) -->
<script>
if (localStorage.theme === 'dark' ||
(!localStorage.theme &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
</head>
<body class="bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
<!-- Header -->
<header class="bg-white dark:bg-gray-800 border-b border-gray-200
dark:border-gray-700 shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
My App
</h1>
<!-- Theme toggle button -->
<button id="theme-toggle"
class="p-2 rounded-lg bg-gray-100 dark:bg-gray-700
hover:bg-gray-200 dark:hover:bg-gray-600
transition-colors duration-200">
<svg class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" 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 0z" clip-rule="evenodd"></path>
</svg>
<svg class="w-5 h-5 block dark:hidden" 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"></path>
</svg>
</button>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 py-8">
<!-- Card Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Card 1 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md
dark:shadow-gray-900/50 p-6 border border-gray-200
dark:border-gray-700 hover:shadow-lg transition-all">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Feature One
</h2>
<p class="text-gray-600 dark:text-gray-300 mb-4">
This is a description that looks good in both light and dark mode.
</p>
<button class="px-4 py-2 bg-blue-500 dark:bg-blue-600
hover:bg-blue-600 dark:hover:bg-blue-700
text-white rounded-md transition-colors">
Learn More
</button>
</div>
<!-- Card 2 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md
dark:shadow-gray-900/50 p-6 border border-gray-200
dark:border-gray-700">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Feature Two
</h2>
<p class="text-gray-600 dark:text-gray-300 mb-4">
Another feature with proper dark mode styling.
</p>
<button class="px-4 py-2 bg-green-500 dark:bg-green-600
hover:bg-green-600 dark:hover:bg-green-700
text-white rounded-md transition-colors">
Get Started
</button>
</div>
<!-- Card 3 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md
dark:shadow-gray-900/50 p-6 border border-gray-200
dark:border-gray-700">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Feature Three
</h2>
<p class="text-gray-600 dark:text-gray-300 mb-4">
Dark mode implementation that respects user preferences.
</p>
<button class="px-4 py-2 bg-purple-500 dark:bg-purple-600
hover:bg-purple-600 dark:hover:bg-purple-700
text-white rounded-md transition-colors">
Explore
</button>
</div>
</div>
</main>
<script>
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
const html = document.documentElement;
const isDark = html.classList.contains('dark');
if (isDark) {
html.classList.remove('dark');
localStorage.theme = 'light';
} else {
html.classList.add('dark');
localStorage.theme = 'dark';
}
});
</script>
</body>
</html>
Preventing Flash of Unstyled Content
To prevent the page from briefly showing the wrong theme on load, add this script in the <head>:
Prevent Theme Flash (FOUC)
<head>
<!-- Place this BEFORE Tailwind CSS loads -->
<script>
// Check localStorage and 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>
<!-- Then load Tailwind CSS -->
<link href="styles.css" rel="stylesheet">
</head>
Practice Exercise
Task: Build a dark mode dashboard:
- Create a navigation bar with logo and dark mode toggle
- Add a sidebar with navigation links (home, profile, settings)
- Build a main content area with 4 stat cards showing metrics
- Add a table with alternating row colors that work in both modes
- Include form elements (input, select, button) with dark mode variants
- Implement the toggle functionality with localStorage persistence
- Ensure all text has proper contrast in both modes
Challenge Exercise
Advanced Task: Create a complete dark mode blog:
- Header with navigation, search bar, and theme toggle with smooth transitions
- Hero section with background image that dims in dark mode
- Blog post grid with cards (image, title, excerpt, read time, tags)
- Different color schemes for tag categories that work in both modes
- Footer with social icons that invert in dark mode
- Comment section with nested replies and proper elevation
- Three-state toggle: Light, Dark, System (follows OS preference)
- Add smooth transitions between theme changes
- Test with WCAG contrast ratios for accessibility
Summary
In this lesson, you've learned:
- How to use the
dark:variant for dark mode styling - The difference between
mediaandclassstrategies - Implementing a complete dark mode toggle with JavaScript
- Design best practices: color choices, shadows, borders
- Handling images effectively in dark mode
- Detecting and responding to system preferences
- Preventing flash of unstyled content (FOUC)
- Creating accessible dark mode experiences
Dark mode is more than just inverting colors—it's about creating a cohesive, comfortable experience in both themes. In the next lesson, we'll explore Tailwind plugins and how to create custom utilities.