
Why Dark Mode?
More than aesthetics, and not as simple as inverting colors.
Dark mode isn't a trend. It's the default for most users now. But building one that works requires intentional design decisions, not a CSS filter.
82%
of smartphone users have dark mode enabled
2024 survey data
39-47%
battery savings on OLED at full brightness
Purdue University, 2021
33%
of US adults have astigmatism, affecting dark mode readability
WebAIM
Accessibility benefits are real. Dark mode reduces total light for photosensitive users and eases eye strain in low-light environments. But it's NOT universally better: astigmatism causes light text on dark backgrounds to appear blurred, a phenomenon called halation. Offering both light and dark as a user choice matters.
OLED savings are real but nuanced. At typical 30-40% auto-brightness, savings are only 3-9%. The significant 39-47% figure only applies at 100% brightness. Worth designing for, but not the primary argument.
Surface & Elevation
Higher means lighter: the opposite of what shadows do in light mode.
In light mode, depth equals shadow, with elements casting darker shapes below to feel raised. In dark mode, shadows are invisible against dark backgrounds. Instead, depth equals surface brightness: higher surfaces are lighter. This is how Material Design, Apple, and most modern dark themes create visual hierarchy.
Material Design recommends a base surface of #121212, NOT pure black. Pure black (#000000) eliminates the range available for elevation levels and causes OLED ghosting artifacts during scrolling.

Why Not Shadows?
Light Mode
Shadow creates depth ✓
Dark Mode
Shadow disappears ✗
On dark backgrounds, drop shadows blend into the surrounding darkness and lose their visual effect. Instead, use elevated surface colors combined with subtle borders at 5-10% white opacity to communicate depth and layering.
Color Adaptation
Why your brand blue needs a different shade in the dark.
Saturated colors vibrate and glow on dark backgrounds, an optical effect where color appears to pulse at its edges. This isn't just ugly; highly saturated hues on dark surfaces frequently fail WCAG contrast requirements. You can't simply reuse your light-mode palette and call it done.
The fix: desaturate by roughly 20 points and increase lightness by 10-15 points. Material Design uses the tonal palette 200-400 range for dark surfaces (versus 500-700 for light). Keeping the same hue family maintains brand recognition while ensuring readability and visual comfort.

Light Mode
Dark Mode
→
H: 208° → 208° | S: 79% → 59% (-20) | L: 51% → 66% (+15)
Light Background
BadgeLink text
Dark Background
BadgeLink text
How the Pros Do It
Slack
GitHub
Twitter / X
Typography & Contrast
Why pure white on pure black is technically accessible but practically unreadable.

Halation is where bright text appears to glow and blur against dark backgrounds. Pure white (#FFFFFF) on pure black (#000000) yields a 21:1 contrast ratio, which exceeds WCAG AAA. But that maximum contrast creates visual discomfort and perceived blur, especially for the roughly 33% of adults who have some degree of astigmatism.
The practical sweet spot: a background of #121212-#1A1A1A paired with text of #E0E0E0-#F0F0F0 gives approximately 13:1-15:1 contrast. That's well above WCAG AA (4.5:1 for normal text) while avoiding halation. There is no “too much contrast” rule in WCAG, but usability research shows reading comfort drops above roughly 15:1.
The quick brown fox jumps over the lazy dog. Reading long passages of text in dark mode should feel effortless, with no squinting and no glowing edges, no visual fatigue after minutes of sustained reading. Comfortable contrast is the goal.
Contrast
21.0:1
WCAG
AAA
Background
Text
Material Design Text Emphasis Levels
High emphasis: primary text and headings
(87%)Medium emphasis: secondary text
(60%)Disabled: inactive labels and hints
(38%)Font Weight Shifts in Dark Mode
Text appears optically bolder on dark backgrounds because light disperses outward against surrounding darkness, the same halation effect at a smaller scale. Body text set at weight 400 in light mode may need to remain at 400 or even drop to 300 in dark mode to maintain the same perceived weight. Medium weight (500) can read like semibold (600). If your dark theme feels “heavy,” try reducing font weights by one step before adjusting sizes.
Semantic Token Mapping
One component, two themes: the architecture that makes it work.
The key insight: components reference semantic tokens, which resolve to different primitive values per theme. The component layer never knows which theme is active. Theme switching only requires redefining semantic-to-primitive mappings.
Primitive
gray-900:
blue-300:
Semantic
dark themebg-primary → gray-900
interactive → blue-300
Component
card-bg → bg-primary
button-bg → interactive
:root {
--bg-primary: #ffffff;
--bg-surface: #f5f5f5;
--text-primary: #1a1a1a;
--text-secondary: #6b7280;
--interactive: #1e88e5;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #121212;
--bg-surface: #1e1e1e;
--text-primary: #e0e0e0;
--text-secondary: #9e9e9e;
--interactive: #64b5f6;
}
}Images, Icons & Shadows
Adapting images, icons, and depth cues for dark backgrounds.
Three categories need adaptation: images (reduce brightness to avoid “light-bombing”), icons (use currentColor, prefer outlined style), and depth cues (shadows to borders and elevation).
Transparent background assets are the biggest pitfall. A dark logo on a transparent PNG disappears on dark backgrounds. SVGs with hard-coded fill colors won't adapt.

Card Heading
First line of body text for this demo card.
Second line shows depth via shadow.
Light mode: shadow works
Card Heading
First line of body text for this demo card.
Shadow is invisible against dark.
Dark mode: shadow invisible
Card Heading
First line of body text for this demo card.
Subtle border defines the edge.
Elevated surface + border
Card Heading
First line of body text for this demo card.
Inner glow adds soft definition.
Elevated surface + inner glow
Do's and Don'ts
- Provide alternate logo versions for dark mode
- Use
filter: brightness(0.8) contrast(1.1)on photographic images - Use
currentColorin SVG icons for automatic adaptation
- Assume transparent PNGs work on any background
- Use bright/white shadows as replacement (looks like glowing artifacts)
- Forget screenshots and illustrations with assumed-white backgrounds
Implementation Patterns
Five approaches, from pure CSS to full JavaScript control.

From declarative CSS to programmatic JavaScript, each approach has tradeoffs. Most production apps combine several, such as a CSS media query for the initial load, a JS toggle for user override, and a cookie for SSR persistence.
/* Detects OS-level preference, no JS needed */
@media (prefers-color-scheme: dark) {
:root {
--bg: #121212;
--text: #e0e0e0;
--accent: #64b5f6;
}
}
body {
background: var(--bg, #ffffff);
color: var(--text, #1a1a1a);
}Avoiding Flash of Wrong Theme (FOUC)
The most common dark-mode bug: the page loads in light mode, then flashes to dark after JavaScript runs. The fix is a tiny blocking script in the document head.
<!-- Place in <head> before any CSS -->
<script>
(function() {
var theme = localStorage.getItem('theme');
if (!theme) {
theme = matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
}
document.documentElement.classList.add(theme);
})();
</script>For SSR frameworks (Next.js, Nuxt), use cookies instead of localStorage, read server-side and injected as a class on <html> before hydration.
Common Mistakes
The eight most common dark mode failures, and how to fix each one.
Dark mode seems simple until you test it. These are the mistakes that show up in almost every first attempt, and each one degrades the experience in a different way.
✗Using #000000 causes halation, feels like a void, and produces OLED ghosting on scroll.
✓Use #121212 to #1A1A1A, which provides depth range for elevation.
✗Reusing light-mode accent colors, which vibrate and glow on dark backgrounds.
✓Desaturate ~20 points and increase lightness for comfortable contrast.
✗Light-mode disabled styles become invisible on dark backgrounds.
✓Audit every interactive state (hover, focus, disabled, placeholder) in dark mode.
✗Some cards elevated (lighter), others flat, giving a confusing visual hierarchy.
✓Apply a systematic elevation scale across all surfaces.
✗No auto-detection, forcing users to toggle manually.
✓Implement prefers-color-scheme detection + matchMedia change listener.
✗Using filter: invert(1), which destroys images, shifts brand colors and loses semantic meaning.
✓Design dark mode intentionally with adapted colors, not CSS filters.
✗Dark logos on transparent PNGs disappear on dark backgrounds.
✓Provide alternate logo/icon versions or use CSS filters selectively on SVGs.
✗Text appears bolder in dark mode due to light dispersal against dark.
✓Audit font weights: body at 400 may look like 500, so consider reducing by one step.
Testing Checklist
A systematic checklist for shipping dark mode with confidence.
Don't ship dark mode without testing it systematically. OLED and LCD render differently, DevTools emulation misses edge cases, and automated tools only catch contrast ratios, not aesthetic issues.
0 of 22 complete
Contrast
Interactive States
Assets
Behavior
Edge Cases
Glossary
Key terms used throughout this guide.
| Term | Definition | Section |
|---|---|---|
| Color-scheme | CSS property that tells the browser which themes a page supports, adapting native UI elements. | §7 |
| Elevation | Visual depth layer. In dark mode, higher elevation = lighter surface color. | §2 |
| FOUC | Flash of Unstyled Content: the brief flash of wrong theme before JS applies the correct one. | §7 |
| Halation | Optical effect where bright text glows/blurs against dark backgrounds, especially with astigmatism. | §4 |
| light-dark() | CSS function that returns one of two values based on the computed color-scheme. | §7 |
| matchMedia | JavaScript API for programmatically detecting media features like prefers-color-scheme. | §7 |
| OLED | Display technology where pixels emit their own light. True black (#000) pixels are fully off, saving power. | §1 |
| prefers-color-scheme | CSS media feature that detects the user's OS-level light/dark preference. | §7 |
| Semantic token | A design token that describes purpose (bg-primary) rather than value (gray-900). | §5 |
| Surface | A background plane in the UI. Dark mode surfaces use lighter grays for higher elevation. | §2 |
| Tonal palette | A range of lightness steps for a single hue, used to pick light/dark variants. | §3 |
| Vibrancy | Apple's system effect that blends content with what's behind it for depth. | §2 |
| WCAG | Web Content Accessibility Guidelines: defines minimum contrast ratios for text and UI elements. | §4 |
Sources
References, research, and recommended reading.
Research & Data
Purdue University: OLED battery savings (ACM MobiSys 2021)
Quantified dark mode power savings at various brightness levels
WebAIM: Dark mode accessibility recommendations (2025)
Guidance on astigmatism, contrast, and offering user choice
Nielsen Norman Group: Dark mode user preference data (2024)
Survey data on adoption rates across platforms
Design Systems
Material Design 3: Dark Theme
Elevation overlays, tonal surfaces, color adaptation guidelines
Apple Human Interface Guidelines: Dark Mode
Vibrancy, materials, semantic background levels
Slack Engineering: Building Dark Mode on Desktop
Semantic token restructuring, CSS variable strategy
GitHub: Primer color system and inclusive design
Multiple dark themes, Primer Prism tool, 1000+ use case audit
Implementation References
- MDN: light-dark() CSS function
Reference and browser support
- web.dev: prefers-color-scheme guide
Implementation patterns and best practices
- Tailwind CSS: Dark Mode
Media and class strategies for Tailwind v4
- CSS-Tricks: light-dark() almanac
Practical usage guide