Tailwind CSS

Dark Mode

20 min Lesson 24 of 35

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.

When to use media strategy:
  • 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>
Class strategy is recommended for most applications because it gives you full control over dark mode toggling, allows you to respect user preferences while offering manual override, and enables you to persist the user's choice.

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:

  1. Create a navigation bar with logo and dark mode toggle
  2. Add a sidebar with navigation links (home, profile, settings)
  3. Build a main content area with 4 stat cards showing metrics
  4. Add a table with alternating row colors that work in both modes
  5. Include form elements (input, select, button) with dark mode variants
  6. Implement the toggle functionality with localStorage persistence
  7. Ensure all text has proper contrast in both modes

Challenge Exercise

Advanced Task: Create a complete dark mode blog:

  1. Header with navigation, search bar, and theme toggle with smooth transitions
  2. Hero section with background image that dims in dark mode
  3. Blog post grid with cards (image, title, excerpt, read time, tags)
  4. Different color schemes for tag categories that work in both modes
  5. Footer with social icons that invert in dark mode
  6. Comment section with nested replies and proper elevation
  7. Three-state toggle: Light, Dark, System (follows OS preference)
  8. Add smooth transitions between theme changes
  9. Test with WCAG contrast ratios for accessibility
Pro Tip: Always test your dark mode in an actual dark environment. What looks good on a bright screen might be too bright or have insufficient contrast in a dark room. Use browser dev tools to emulate dark mode for testing.

Summary

In this lesson, you've learned:

  • How to use the dark: variant for dark mode styling
  • The difference between media and class strategies
  • 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.