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