Dark Mode with CSS
Why Dark Mode?
Dark mode reduces eye strain in low-light environments, saves battery on OLED screens, and matches user OS preferences. Modern sites should respect prefers-color-scheme and optionally offer a manual toggle.
Detect System Preference
:root {
--bg: #ffffff;
--text: #212529;
--border: #dee2e6;
--accent: #007bff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a2e;
--text: #e9ecef;
--border: #343a40;
--accent: #4dabf7;
}
}
body {
background: var(--bg);
color: var(--text);
}
Use CSS custom properties for all theme-dependent values — one media query switches the entire theme.
The color-scheme Property
Tell the browser to style native UI (scrollbars, form controls) for dark mode:
:root {
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}
Without this, <input> and <select> may stay light-themed in dark pages.
Manual Theme Toggle
Support user override of system preference with a data-theme attribute:
:root,
[data-theme="light"] {
--bg: #ffffff;
--text: #212529;
}
[data-theme="dark"] {
--bg: #1a1a2e;
--text: #e9ecef;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #1a1a2e;
--text: #e9ecef;
}
}
const toggle = document.querySelector('#theme-toggle');
const stored = localStorage.getItem('theme');
if (stored) {
document.documentElement.setAttribute('data-theme', stored);
}
toggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
Prevent Flash of Wrong Theme (FOUT)
Inline script in <head> before CSS to apply stored theme immediately:
<script>
(function() {
const theme = localStorage.getItem('theme');
if (theme) document.documentElement.setAttribute('data-theme', theme);
})();
</script>
Meta Theme Color
Update mobile browser chrome per theme:
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1a1a2e" media="(prefers-color-scheme: dark)">
Images and Media in Dark Mode
@media (prefers-color-scheme: dark) {
.logo { content: url('/logo-dark.svg'); }
img:not([src*=".svg"]) {
opacity: 0.9;
}
}
Or provide separate assets:
<picture>
<source srcset="diagram-dark.svg" media="(prefers-color-scheme: dark)">
<img src="diagram-light.svg" alt="Architecture diagram">
</picture>
Shadows and Borders in Dark Mode
Pure black shadows disappear on dark backgrounds — use lighter, subtle borders:
[data-theme="dark"] .card {
background: #16213e;
border: 1px solid #343a40;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
Contrast and Accessibility
Dark mode is not just inverted colors — maintain WCAG contrast ratios:
- Body text: 4.5:1 minimum against background
- Muted text: don’t go too dim (
#6c757don#1a1a2emay fail) - Test with WebAIM Contrast Checker
Avoid pure #000 background with pure #fff text — use off-white (#e9ecef) and dark gray (#1a1a2e) to reduce halation.
prefers-reduced-motion
Pair dark mode with motion preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Design Token Structure
:root {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f8f9fa;
--color-text-primary: #212529;
--color-text-secondary: #6c757d;
--color-accent: #007bff;
}
[data-theme="dark"] {
--color-bg-primary: #1a1a2e;
--color-bg-secondary: #16213e;
--color-text-primary: #e9ecef;
--color-text-secondary: #adb5bd;
--color-accent: #4dabf7;
}
Semantic token names (bg-primary) survive theme changes without renaming.
Best Practices
- Default to system preference — don’t force dark mode
- Persist manual choice in
localStorage - Use semantic CSS variables — never hardcode colors in components
- Set
color-schemefor native form controls - Test both themes in CI or design review
Troubleshooting
Toggle doesn’t override system theme
- Ensure
[data-theme="dark"]selector specificity beats media query - Use
:root:not([data-theme="light"])pattern for system fallback
Flash of light theme on load
- Add inline script in
<head>before body renders
Images look wrong in dark mode
- Provide dark variants or adjust opacity/borders
Dark mode is a user experience expectation — implement it with CSS variables, system detection, and accessible contrast.