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

AspectLight ModeDark Mode
BackgroundWhite/light grayDark gray (not pure black)
TextDark gray/blackLight gray (not pure white)
ElevationShadowsLighter surfaces
SaturationHigherLower (colors are too vivid)
ContrastHigh contrast safeSlightly 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
: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
: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

ColorLight ModeDark Mode Adjustment
PrimaryHSL(220, 80%, 50%)Lighter, less saturated: HSL(220, 65%, 60%)
SuccessHSL(142, 70%, 40%)Lighter: HSL(142, 60%, 55%)
WarningHSL(38, 90%, 50%)Less saturated: HSL(38, 75%, 55%)
ErrorHSL(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 LevelDark 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

ElementLight ModeDark Mode
BackgroundWhite / Light grayDark gray (8-12%)
TextDark gray (15%)Off-white (90%)
ShadowsUse freelyMinimize, use lightness
SaturationNormalReduce 15-25%
BordersDarker than bgLighter than bg
Primary colorStandardLighter + desaturated

Next: 10-tools-reference.md - Essential tools and quick reference