🌓 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