Dark Mode Design
Dark mode isn't just inverted colors. It requires thoughtful adjustments to maintain readability, hierarchy, and brand consistency.
Why Dark Mode?
- Reduced eye strain in low-light environments
- Battery savings on OLED screens (true black = pixels off)
- User preference - many users simply prefer it
- Accessibility - some users with light sensitivity need it
- Modern expectation - users expect the option
Dark Mode Fundamentals
Don't Just Invert
Simply inverting colors creates problems:
/* DON'T: Simple inversion */
.dark-mode {
filter: invert(1); /* Inverts everything, including images */
}
/* DON'T: Swap black and white */
.dark-mode {
--text: white;
--bg: black; /* Pure black is too harsh */
}
Key Differences from Light Mode
| Aspect | Light Mode | Dark Mode |
|---|---|---|
| Background | White/light gray | Dark gray (not pure black) |
| Text | Dark gray/black | Light gray (not pure white) |
| Elevation | Shadows | Lighter surfaces |
| Saturation | Higher | Lower (colors are too vivid) |
| Contrast | High contrast safe | Slightly reduced contrast |
Choosing Dark Mode Backgrounds
Avoid Pure Black
Pure black (#000) causes problems:
- Harsh contrast with white text (halation effect)
- Looks like a "hole" in the screen
- Doesn't allow for elevation through lightness
Recommended Background Values
:root {
/* Page background - darkest */
--dark-bg-page: hsl(220, 15%, 8%);
/* Card/surface background - slightly lighter */
--dark-bg-surface: hsl(220, 15%, 12%);
/* Elevated surfaces - even lighter */
--dark-bg-elevated: hsl(220, 15%, 16%);
/* Hover states */
--dark-bg-hover: hsl(220, 15%, 20%);
}
True Black for OLED (Optional)
Some apps offer a "true black" option for OLED battery savings:
:root {
/* Standard dark mode */
--dark-bg: hsl(220, 15%, 10%);
/* OLED/true black mode */
--oled-bg: hsl(0, 0%, 0%);
}
Text Colors in Dark Mode
Don't Use Pure White
Pure white text on dark backgrounds:
- Causes eye strain
- Creates "glow" effect
- Makes long-form reading difficult
Recommended Text Values
:root {
/* Primary text - off-white */
--dark-text-primary: hsl(220, 15%, 92%);
/* Secondary text */
--dark-text-secondary: hsl(220, 10%, 70%);
/* Muted text */
--dark-text-muted: hsl(220, 10%, 55%);
/* Disabled text */
--dark-text-disabled: hsl(220, 10%, 40%);
}
Reduce Contrast Slightly
In dark mode, AAA contrast (7:1) can be too harsh. AA (4.5:1) is comfortable:
/* Light mode: 14:1 contrast ratio */
.light {
color: hsl(220, 15%, 15%);
background: white;
}
/* Dark mode: 11:1 contrast ratio - easier on eyes */
.dark {
color: hsl(220, 15%, 90%);
background: hsl(220, 15%, 10%);
}
Adjusting Brand Colors
Desaturate Vivid Colors
Saturated colors that work on light backgrounds become overwhelming on dark:
/* Light mode primary */
.light {
--primary: hsl(220, 80%, 50%); /* Vibrant blue */
}
/* Dark mode - reduce saturation, adjust lightness */
.dark {
--primary: hsl(220, 65%, 55%); /* Softer, slightly lighter */
}
Increase Lightness for Readability
/* Light mode link color */
.light {
--link: hsl(220, 80%, 45%); /* 4.5:1 on white ✓ */
}
/* Dark mode - needs to be lighter to maintain contrast */
.dark {
--link: hsl(220, 70%, 60%); /* 4.5:1 on dark bg ✓ */
}
Color Adjustment Guidelines
| Color | Light Mode | Dark Mode Adjustment |
|---|---|---|
| Primary | HSL(220, 80%, 50%) | Lighter, less saturated: HSL(220, 65%, 60%) |
| Success | HSL(142, 70%, 40%) | Lighter: HSL(142, 60%, 55%) |
| Warning | HSL(38, 90%, 50%) | Less saturated: HSL(38, 75%, 55%) |
| Error | HSL(0, 75%, 50%) | Lighter: HSL(0, 65%, 60%) |
Elevation and Depth
Light Mode: Shadows Create Depth
.light .card {
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.light .card-elevated {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
Dark Mode: Lighter Surfaces Create Depth
Shadows don't work well on dark backgrounds. Use surface lightness instead:
.dark .card {
background: hsl(220, 15%, 14%); /* Slightly lighter than page */
box-shadow: none; /* Or very subtle */
}
.dark .card-elevated {
background: hsl(220, 15%, 18%); /* Even lighter */
}
.dark .modal {
background: hsl(220, 15%, 22%); /* Highest elevation */
}
Material Design Elevation Scale
| Elevation Level | Dark Mode Lightness |
|---|---|
| 0 (Page) | 8% |
| 1 (Card) | 12% |
| 2 (Raised card) | 14% |
| 3 (Modal) | 16% |
| 4 (Dropdown) | 18% |
| 5 (Top bar) | 20% |
Borders and Dividers
Adjust Border Colors
.light {
--border-subtle: hsl(220, 15%, 90%);
--border-default: hsl(220, 15%, 82%);
}
.dark {
--border-subtle: hsl(220, 15%, 18%);
--border-default: hsl(220, 15%, 25%);
}
Consider Using Lighter Backgrounds Instead
Sometimes borders aren't needed in dark mode if elevation handles it:
.dark .card {
background: hsl(220, 15%, 14%);
border: none; /* Surface color difference is enough */
}
Implementing Dark Mode in CSS
Using CSS Custom Properties
:root {
/* Default (light mode) */
--bg-page: hsl(220, 15%, 98%);
--bg-surface: white;
--text-primary: hsl(220, 15%, 15%);
--text-secondary: hsl(220, 10%, 45%);
--primary: hsl(220, 80%, 50%);
--border: hsl(220, 15%, 85%);
}
/* Dark mode */
[data-theme="dark"] {
--bg-page: hsl(220, 15%, 8%);
--bg-surface: hsl(220, 15%, 12%);
--text-primary: hsl(220, 15%, 92%);
--text-secondary: hsl(220, 10%, 65%);
--primary: hsl(220, 65%, 60%);
--border: hsl(220, 15%, 22%);
}
/* Or use media query for system preference */
@media (prefers-color-scheme: dark) {
:root {
--bg-page: hsl(220, 15%, 8%);
/* ... */
}
}
Switching Implementation
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Toggle theme
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
// Initialize
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
} else if (prefersDark) {
setTheme('dark');
}
Complete Dark Mode Color System
:root {
/* ========== LIGHT MODE (Default) ========== */
/* Backgrounds */
--bg-page: hsl(220, 15%, 97%);
--bg-surface: hsl(0, 0%, 100%);
--bg-elevated: hsl(0, 0%, 100%);
--bg-muted: hsl(220, 15%, 94%);
--bg-hover: hsl(220, 15%, 92%);
/* Text */
--text-primary: hsl(220, 15%, 15%);
--text-secondary: hsl(220, 10%, 40%);
--text-muted: hsl(220, 10%, 55%);
--text-disabled: hsl(220, 10%, 70%);
/* Borders */
--border-subtle: hsl(220, 15%, 92%);
--border-default: hsl(220, 15%, 85%);
--border-strong: hsl(220, 15%, 70%);
/* Primary */
--primary: hsl(220, 80%, 50%);
--primary-hover: hsl(220, 80%, 45%);
--primary-active: hsl(220, 80%, 40%);
--primary-subtle: hsl(220, 80%, 95%);
/* Semantic */
--success: hsl(142, 70%, 42%);
--success-subtle: hsl(142, 70%, 94%);
--warning: hsl(38, 90%, 50%);
--warning-subtle: hsl(38, 90%, 94%);
--error: hsl(0, 75%, 50%);
--error-subtle: hsl(0, 75%, 95%);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
/* ========== DARK MODE ========== */
/* Backgrounds */
--bg-page: hsl(220, 18%, 8%);
--bg-surface: hsl(220, 15%, 12%);
--bg-elevated: hsl(220, 15%, 16%);
--bg-muted: hsl(220, 15%, 14%);
--bg-hover: hsl(220, 15%, 18%);
/* Text */
--text-primary: hsl(220, 15%, 92%);
--text-secondary: hsl(220, 10%, 68%);
--text-muted: hsl(220, 10%, 52%);
--text-disabled: hsl(220, 10%, 38%);
/* Borders */
--border-subtle: hsl(220, 15%, 18%);
--border-default: hsl(220, 15%, 25%);
--border-strong: hsl(220, 15%, 35%);
/* Primary - lighter and less saturated */
--primary: hsl(220, 65%, 58%);
--primary-hover: hsl(220, 65%, 52%);
--primary-active: hsl(220, 65%, 48%);
--primary-subtle: hsl(220, 50%, 18%);
/* Semantic - adjusted for dark backgrounds */
--success: hsl(142, 60%, 52%);
--success-subtle: hsl(142, 50%, 15%);
--warning: hsl(38, 80%, 55%);
--warning-subtle: hsl(38, 60%, 15%);
--error: hsl(0, 65%, 58%);
--error-subtle: hsl(0, 50%, 15%);
/* Shadows - more subtle in dark mode */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
}
Dark Mode Checklist
Colors
- [ ] Background is dark gray, not pure black
- [ ] Text is off-white, not pure white
- [ ] Brand colors are desaturated and lightened
- [ ] Semantic colors adjusted for dark backgrounds
- [ ] Contrast ratios meet AA (4.5:1 for text)
Elevation
- [ ] Shadows reduced or removed
- [ ] Elevated surfaces use lighter backgrounds
- [ ] Hierarchy is still clear
Elements
- [ ] Form inputs have visible borders
- [ ] Focus states are visible
- [ ] Disabled states are distinguishable
- [ ] Links are visible and accessible
- [ ] Dividers/borders adjusted
Images & Media
- [ ] Logo has dark mode variant (if needed)
- [ ] Images don't look washed out
- [ ] Icons maintain visibility
- [ ] Consider reducing image brightness
Testing
- [ ] Test in actual dark environment
- [ ] Check on different screens (OLED vs LCD)
- [ ] Verify all interactive states
- [ ] Test color blind modes
Common Dark Mode Mistakes
1. Pure Black Backgrounds
Use dark gray (8-12% lightness) instead.
2. Pure White Text
Use off-white (88-92% lightness) instead.
3. Unchanged Saturated Colors
Reduce saturation and increase lightness.
4. Using Shadows for Elevation
Use surface lightness instead.
5. Forgetting About Images
Consider darkening overlays or dark mode image variants.
Summary
| Element | Light Mode | Dark Mode |
|---|---|---|
| Background | White / Light gray | Dark gray (8-12%) |
| Text | Dark gray (15%) | Off-white (90%) |
| Shadows | Use freely | Minimize, use lightness |
| Saturation | Normal | Reduce 15-25% |
| Borders | Darker than bg | Lighter than bg |
| Primary color | Standard | Lighter + desaturated |
Next: 10-tools-reference.md - Essential tools and quick reference