
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 β elements cast 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 β 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 β no squinting, 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 β 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 inject the 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, OLED ghosting on scroll.
βUse #121212 to #1A1A1A β provides depth range for elevation.
βReusing light-mode accent colors β they 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 β confusing visual hierarchy.
βApply a systematic elevation scale across all surfaces.
βNo auto-detection β forcing users to manually toggle.
βImplement prefers-color-scheme detection + matchMedia change listener.
βUsing filter: invert(1) β destroys images, shifts brand colors, 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; 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