🌓 Dark Mode Toggle Design Guide

Dark mode (або "night mode") є essential accessibility feature що reduces eye strain, saves battery на OLED screens, та provides comfortable reading у low-light environments. З 60%+ users preferring dark mode, implementing theme toggle стало standard expectation для modern websites. Цей guide охоплює design patterns, color schemes, localStorage persistence, та smooth transitions.

👆 Try the toggle у top-right corner!

Click the switch to see this entire page transform між light та dark modes. Notice smooth transitions, adjusted colors, та proper contrast ratios.

🎨 Color Scheme Design

Light Mode Palette:

  • Background: #FFFFFF (pure white) або #F8F9FA (off-white)
  • Text: #2C3650 (dark gray, not pure black)
  • Accents: #667EEA (vibrant colors)
  • Borders: #E9ECEF (light gray)
  • Cards: #FFFFFF з subtle shadow

Dark Mode Palette:

  • Background: #1A202C (dark blue-gray, not pure black)
  • Surface: #2D3748 (cards, panels)
  • Text: #E2E8F0 (light gray, not pure white)
  • Accents: #A78BFA (desaturated, lighter purples)
  • Borders: #4A5568 (medium gray)

⚠️ Avoid pure black (#000): Causes eye strain, reduces contrast readability

WCAG Contrast Requirements:

  • Normal text: 4.5:1 minimum (Level AA)
  • Large text (18px+ або 14px+ bold): 3:1 minimum
  • UI components: 3:1 minimum
  • Tool: WebAIM Contrast Checker (webaim.org/resources/contrastchecker)

💻 Complete Implementation

1. CSS Custom Properties (Variables):

/* Define theme variables */
:root {
  /* Light mode (default) */
  --bg-primary: #FFFFFF;
  --bg-secondary: #F8F9FA;
  --text-primary: #2C3E50;
  --text-secondary: #6C757D;
  --accent-primary: #667EEA;
  --accent-hover: #5568D3;
  --border-color: #E9ECEF;
  --shadow: rgba(0, 0, 0, 0.1);
}

[data-theme="dark"] {
  /* Dark mode overrides */
  --bg-primary: #1A202C;
  --bg-secondary: #2D3748;
  --text-primary: #E2E8F0;
  --text-secondary: #A0AEC0;
  --accent-primary: #A78BFA;
  --accent-hover: #C084FC;
  --border-color: #4A5568;
  --shadow: rgba(0, 0, 0, 0.5);
}

/* Apply variables */
body {
  background: var(--bg-primary);
  color: var(--text-primary);
  transition: background-color 0.3s, color 0.3s;
}

.card {
  background: var(--bg-secondary);
  border-color: var(--border-color);
  box-shadow: 0 4px 12px var(--shadow);
}

a {
  color: var(--accent-primary);
}

a:hover {
  color: var(--accent-hover);
}

2. Toggle Button HTML:

<!-- Option 1: Simple Sun/Moon Toggle -->
<button class="theme-toggle" 
        aria-label="Toggle dark mode" 
        id="themeToggle">
  <svg class="sun-icon" viewBox="0 0 24 24">
    <circle cx="12" cy="12" r="5"/>
    <path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>
  </svg>
  <svg class="moon-icon" viewBox="0 0 24 24" style="display: none;">
    <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
  </svg>
</button>

<!-- Option 2: Slider Toggle -->
<div class="theme-toggle-wrapper">
  <input type="checkbox" id="darkModeToggle" class="theme-checkbox">
  <label for="darkModeToggle" class="theme-label">
    <span class="theme-label-text">Dark Mode</span>
    <div class="theme-switch">
      <div class="theme-slider"></div>
    </div>
  </label>
</div>

3. JavaScript Implementation:

// Theme Toggle Class
class ThemeToggle {
  constructor() {
    this.theme = this.getStoredTheme() || this.getPreferredTheme();
    this.toggleBtn = document.getElementById('themeToggle');
    
    this.init();
  }
  
  init() {
    // Set initial theme
    this.setTheme(this.theme);
    
    // Toggle button event
    this.toggleBtn?.addEventListener('click', () => {
      this.toggleTheme();
    });
    
    // Listen для system preference changes
    window.matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', (e) => {
        if (!this.getStoredTheme()) {
          this.setTheme(e.matches ? 'dark' : 'light');
        }
      });
  }
  
  getPreferredTheme() {
    // Check system preference
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light';
  }
  
  getStoredTheme() {
    // Check localStorage
    return localStorage.getItem('theme');
  }
  
  setTheme(theme) {
    this.theme = theme;
    
    // Update DOM
    document.documentElement.setAttribute('data-theme', theme);
    document.body.classList.toggle('dark-mode', theme === 'dark');
    
    // Store preference
    localStorage.setItem('theme', theme);
    
    // Update toggle button UI
    this.updateToggleBtn(theme);
  }
  
  toggleTheme() {
    const newTheme = this.theme === 'light' ? 'dark' : 'light';
    this.setTheme(newTheme);
  }
  
  updateToggleBtn(theme) {
    const sunIcon = this.toggleBtn?.querySelector('.sun-icon');
    const moonIcon = this.toggleBtn?.querySelector('.moon-icon');
    
    if (theme === 'dark') {
      sunIcon && (sunIcon.style.display = 'none');
      moonIcon && (moonIcon.style.display = 'block');
    } else {
      sunIcon && (sunIcon.style.display = 'block');
      moonIcon && (moonIcon.style.display = 'none');
    }
  }
}

// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
  new ThemeToggle();
});

// Prevent Flash of Unstyled Content (FOUC)
(function() {
  const theme = localStorage.getItem('theme') || 
                (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  document.documentElement.setAttribute('data-theme', theme);
  if (theme === 'dark') document.body.classList.add('dark-mode');
})();

🎨 Toggle Button Styles

Toggle Design Options:

1. Slider Switch (iOS-style)

.theme-toggle {
  width: 60px;
  height: 30px;
  background: #e9ecef;
  border-radius: 15px;
  position: relative;
  cursor: pointer;
  transition: background 0.3s;
}

.dark-mode .theme-toggle {
  background: #4a5568;
}

.theme-slider {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  background: #667eea;
  border-radius: 50%;
  transition: transform 0.3s;
}

.dark-mode .theme-slider {
  transform: translateX(30px);
  background: #a78bfa;
}

2. Icon Toggle (Sun/Moon)

.theme-toggle {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 10px;
  border-radius: 50%;
  transition: background 0.2s;
}

.theme-toggle:hover {
  background: rgba(0,0,0,0.05);
}

.theme-icon {
  width: 24px;
  height: 24px;
  transition: transform 0.3s;
}

.theme-toggle:hover .theme-icon {
  transform: rotate(180deg);
}

⚡ Best Practices

1. Respect System Preference

Detect та respect user's OS-level dark mode setting:

@media (prefers-color-scheme: dark) {
  :root {
    /* Dark mode colors */
  }
}

2. Persist User Choice

Store preference у localStorage:

localStorage.setItem('theme', 'dark');

3. Avoid FOUC (Flash of Unstyled Content)

Set theme BEFORE page render:

<script>
  (function() {
    const theme = localStorage.getItem('theme');
    if (theme === 'dark') {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  })();
</script>

4. Smooth Transitions

  • ✅ Transition duration: 200-300ms
  • ✅ Properties: background-color, color, border-color
  • ❌ Don't transition: all (performance)

5. Images та Media

Different images для light/dark modes:

<picture>
  <source srcset="image-dark.jpg" media="(prefers-color-scheme: dark)">
  <img src="image-light.jpg" alt="Mushroom">
</picture>

✅ Dark Mode Checklist

Implementation Checklist:

  • ☐ CSS custom properties defined для both themes
  • ☐ Toggle button accessible (ARIA labels)
  • ☐ System preference detected (prefers-color-scheme)
  • ☐ User choice persisted (localStorage)
  • ☐ FOUC prevented (blocking script)
  • ☐ Smooth transitions (200-300ms)
  • ☐ All text meets WCAG contrast (4.5:1)
  • ☐ Images adapted (або hidden/replaced)
  • ☐ Syntax highlighting adjusted
  • ☐ Focus indicators visible у both modes
  • ☐ Shadows adjusted (lighter у dark mode)
  • ☐ Tested у both modes thoroughly