Dark Mode Design
Design and implement dark mode - color pairing principles, elevation surfaces, CSS variable strategies, semantic tokens, and OLED-safe color choices.
skill: Dark Mode Design category: Design tags: [Dark Mode, Color Theory, CSS Variables, Design Tokens, Accessibility] readingTime: 4
Dark mode done right is not just inverting colors. Done wrong, it's high-contrast eye strain. Done right, it reduces eye fatigue in dim environments, saves battery on OLED screens, and feels like a premium product touch. The key: design dark mode as a first-class theme, not an afterthought.
Workflow
- Choose dark palette foundationsPure black (#000000) backgrounds create harsh contrast with white text, causing halation (text appears to bleed). Dark-gray backgrounds with measured contrast ratios are more comfortable for extended reading.
- Design elevation with overlaysIn light mode, surfaces get darker as they elevate (shadows). In dark mode, shadows are invisible on dark backgrounds - elevation is communicated through lightness instead.
- Implement with CSS custom propertiesCSS custom properties (variables) enable instant theme switching without reloading. One set of semantic variables maps to light or dark values via data-theme attribute or media query.
- Add a theme toggleDetecting system preference is mandatory; a manual toggle is the user-centric enhancement. Users may prefer dark mode in a light-mode OS environment.
- Test dark mode accessibilityDark mode introduces unique accessibility issues: insufficient contrast, images with white backgrounds visible in dark mode, and focus rings invisible on dark surfaces.
Output Format
Dark mode: #0F1117 base with surface elevation via lightness, semantic CSS variables with data-theme attribute, system preference + manual toggle with FOUC prevention, and contrast ratio testing per WCAG AA.
:root { --bg: #FFFFFF; --surface-1: #F8FAFC; --text: #0F172A }
[data-theme="dark"] { --bg: #0F1117; --surface-1: #181B24; --text: #F1F5F9 }
Elevation: surface-1(5%) → surface-2(8%) → overlay(11%) white overlay
Toggle: localStorage > prefers-color-scheme > data-theme attr
FOUC fix: blocking <script> in <head> sets data-theme before renderExamples
Edge Cases & Gotchas
- CSS variable inheritance and specificity - CSS variables inherit through the DOM. If a component sets its own background, the inherited text color may not have sufficient contrast. Always test dark mode in context (complete screens), not component-by-component.
- Third-party iframes and dark mode - Embedded widgets (Stripe, Intercom, maps) don't receive your CSS variables - they have their own theming. Check if the third-party library has a dark mode option and configure it separately.
- Print stylesheets - Dark mode media queries apply during printing, producing black-background printouts. Add @media print { :root { color-scheme: light; } } to force light mode for printing.
- Video thumbnails on dark surfaces - Video thumbnails with white backgrounds become jarring in dark mode. Apply box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05) to create a subtle outline without changing the thumbnail image.
When to Break These Rules
- Skip dark mode for data-heavy enterprise tools with small, tech-savvy audiences where system preference is sufficient - don't build a manual toggle if your users are 95% in light mode.
- Defer dark mode until v2 when your design system uses hardcoded color values throughout - retrofitting dark mode into a hardcoded system is 5× more expensive than building with semantic tokens from day one.
Reference Files
https://web.dev/prefers-color-scheme/https://m3.material.io/styles/color/dark-theme