Table of Contents
- Understanding Dark Mode: Why It Matters
- Core Concepts: Color Schemes and User Preferences
- 2.1 System-Level Color Schemes
- 2.2 CSS Custom Properties (Variables): The Foundation
- Step-by-Step Implementation
- 3.1 Setting Up CSS Variables for Light/Dark Modes
- 3.2 Respecting System Preferences with
prefers-color-scheme - 3.3 Adding a User Toggle Button
- 3.4 Persisting Preferences with
localStorage
- Advanced Techniques
- 4.1 Smooth Transitions Between Modes
- 4.2 Handling Images and Media
- 4.3 Accessibility Best Practices
- Testing Your Dark Mode Implementation
- Conclusion
- References
1. Understanding Dark Mode: Why It Matters
Dark mode (or dark theme) is a color scheme that uses dark backgrounds (typically black, dark gray, or navy) with light-colored text and UI elements. Its rise in popularity is driven by several key benefits:
- Reduced Eye Strain: In low-light environments, bright screens emit blue light that can cause eye fatigue. Dark mode minimizes this by reducing overall brightness.
- Battery Efficiency: On OLED and AMOLED screens (common in smartphones), dark pixels are “off,” consuming less power than light pixels. Studies show dark mode can reduce battery usage by up to 60% on some devices.
- Accessibility: For users with photosensitivity, migraine disorders, or certain visual impairments (e.g., cataracts), dark mode can make content easier to read.
- User Preference: A 2023 survey by Statista found that 79% of users prefer dark mode on mobile apps, and 61% use it exclusively when available.
2. Core Concepts: Color Schemes and User Preferences
Before diving into code, let’s clarify two foundational concepts: system-level color schemes and CSS custom properties.
2.1 System-Level Color Schemes
Modern operating systems (Windows 10+, macOS 10.14+, iOS 13+, Android 10+) allow users to set a system-wide color scheme (light, dark, or auto). Browsers expose this preference to developers via the prefers-color-scheme CSS media query, enabling websites to respect the user’s system settings by default.
2.2 CSS Custom Properties (Variables): The Foundation
CSS custom properties (or variables) are the backbone of any scalable dark mode implementation. They let you define reusable values (e.g., --color-text, --color-background) and update them dynamically—critical for switching between light and dark modes.
Instead of hardcoding colors in your CSS (e.g., color: #333), you’ll define variables in a root scope (e.g., :root), then reference them throughout your styles. When switching modes, you’ll override these variables, and all dependent styles will update automatically.
3. Step-by-Step Implementation
Let’s build dark mode from scratch, starting with CSS variables, then adding system preference support, a user toggle, and persistence.
3.1 Setting Up CSS Variables for Light/Dark Modes
First, define your color palette for both light and dark modes using CSS variables. We’ll use the :root selector to scope variables globally.
/* Light mode (default) variables */
:root {
--color-background: #ffffff; /* White background */
--color-text: #1a1a1a; /* Near-black text */
--color-primary: #2563eb; /* Blue accent */
--color-secondary: #64748b; /* Gray for secondary text */
--color-border: #e2e8f0; /* Light gray border */
}
/* Dark mode variables (will be activated later) */
[data-theme="dark"] {
--color-background: #0f172a; /* Dark blue-gray background */
--color-text: #f8fafc; /* Off-white text */
--color-primary: #60a5fa; /* Light blue accent */
--color-secondary: #94a3b8; /* Light gray secondary text */
--color-border: #334155; /* Dark gray border */
}
Why data-theme? Using a data attribute (e.g., data-theme="dark") on the <html> element is more flexible than classes. It makes the theme state explicit in the DOM and simplifies targeting in CSS/JS.
3.2 Respecting System Preferences with prefers-color-scheme
Next, use the prefers-color-scheme media query to automatically switch to dark mode if the user’s system is set to dark.
Add this to your CSS:
/* Override variables if system prefers dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0f172a;
--color-text: #f8fafc;
--color-primary: #60a5fa;
--color-secondary: #94a3b8;
--color-border: #334155;
}
}
Now, if the user’s system is set to dark, your site will automatically use dark mode. If not, it will default to light mode.
3.3 Adding a User Toggle with JavaScript
Users may want to override their system preference (e.g., using dark mode on your site even if their system is light). To enable this, add a toggle button.
Step 1: Add the Toggle Button (HTML)
Add a button to your UI (e.g., in the header) to let users switch modes:
<button id="theme-toggle" aria-label="Toggle dark mode">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- Sun icon (default for light mode) -->
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
Step 2: Update the Toggle Icon (CSS)
Use CSS to swap the icon when dark mode is active. We’ll target the data-theme="dark" attribute to show a moon icon instead of the sun:
/* Hide moon icon by default (light mode) */
#theme-toggle .moon-icon {
display: none;
}
/* Show moon icon in dark mode, hide sun */
[data-theme="dark"] #theme-toggle .sun-icon {
display: none;
}
[data-theme="dark"] #theme-toggle .moon-icon {
display: block;
}
(Note: Update the SVG to include both sun and moon icons with appropriate classes, or use an icon library like Font Awesome.)
Step 3: Add Toggle Logic (JavaScript)
Use JavaScript to:
- Check the current theme (system preference or saved user preference).
- Toggle the
data-themeattribute on the<html>element. - Update the toggle icon.
// Get the toggle button and root element
const themeToggle = document.getElementById("theme-toggle");
const root = document.documentElement;
// Check for saved user preference or use system preference
const savedTheme = localStorage.getItem("theme");
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
const initialTheme = savedTheme || systemTheme;
// Set initial theme
root.setAttribute("data-theme", initialTheme);
// Toggle theme when button is clicked
themeToggle.addEventListener("click", () => {
const currentTheme = root.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
// Update root attribute
root.setAttribute("data-theme", newTheme);
// Save preference to localStorage
localStorage.setItem("theme", newTheme);
});
3.4 Persisting Preferences with localStorage
In the code above, we already save the user’s theme choice to localStorage (via localStorage.setItem("theme", newTheme)). On page load, we check for this saved preference and apply it, ensuring the user’s choice persists across sessions.
4. Advanced Techniques
Now that you have a basic dark mode implementation, let’s refine it with advanced features.
4.1 Smooth Transitions
Abrupt color changes can be jarring. Add CSS transitions to make mode switches smooth:
/* Add transitions to all properties using variables */
:root {
transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}
This will animate text, background, and border colors when switching modes.
4.2 Handling Images and Media
Images (especially PNGs with white backgrounds) can clash in dark mode. Use one of these solutions:
-
Dark Mode Variants: Serve a dark-optimized image in dark mode using the
<picture>element:<picture> <source srcset="logo-dark.png" media="(prefers-color-scheme: dark)"> <source srcset="logo-dark.png" media="(data-theme: dark)"> <!-- For user toggle --> <img src="logo-light.png" alt="Logo"> </picture> -
CSS Filters: Desaturate or invert images in dark mode (use sparingly, as it can distort colors):
[data-theme="dark"] .image-light { filter: invert(1) hue-rotate(180deg); /* Invert light images */ }
4.3 Accessibility Best Practices
Dark mode doesn’t automatically make your site accessible—poor contrast or color choices can harm readability. Follow these rules:
-
Contrast Ratios: Ensure text meets WCAG 2.1 standards:
- Normal text: Minimum contrast of 4.5:1 (AA) or 7:1 (AAA).
- Large text (18pt+ or 14pt bold): Minimum contrast of 3:1 (AA) or 4.5:1 (AAA).
Use tools like WebAIM Contrast Checker to verify.
-
Avoid Pure Black/White: Pure black (
#000) and white (#fff) can cause eye strain. Use near-black (#1a1a1a) and off-white (#f8fafc) instead. -
Test for Color Blindness: Use tools like Color Safe to ensure your palette is readable for users with red-green, blue-yellow, or monochromatic color blindness.
5. Testing Your Dark Mode Implementation
Test rigorously to ensure dark mode works across devices, browsers, and user scenarios:
- Cross-Browser Testing: Verify support for
prefers-color-scheme(supported in Chrome 76+, Firefox 67+, Safari 12.1+). For older browsers, default to light mode. - System Preference Sync: Test switching your OS color scheme to ensure the site updates automatically (if no user preference is saved).
- Toggle Behavior: Confirm the toggle button switches modes and persists across page reloads.
- Contrast and Readability: Use Lighthouse (Chrome DevTools > Lighthouse) to audit accessibility; it will flag low-contrast text.
- Mobile Testing: Test on OLED devices to confirm battery savings and readability.
6. Conclusion
Dark mode is no longer a “nice-to-have”—it’s a user expectation. By combining CSS custom properties, prefers-color-scheme, and JavaScript, you can implement a robust, accessible dark mode that respects both system settings and user choice.
Key takeaways:
- Use CSS variables to define light/dark palettes for easy theming.
- Respect system preferences with
prefers-color-scheme. - Add a user toggle with
localStoragepersistence. - Prioritize accessibility (contrast, smooth transitions, image handling).
With these steps, you’ll create a dark mode that enhances user experience and keeps your site modern and inclusive.