Why Dark Mode?

Dark mode reduces eye strain in low-light environments, saves battery on OLED screens, and matches user OS preferences. Modern sites should respect prefers-color-scheme and optionally offer a manual toggle.

Detect System Preference

  :root {
    --bg: #ffffff;
    --text: #212529;
    --border: #dee2e6;
    --accent: #007bff;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #1a1a2e;
        --text: #e9ecef;
        --border: #343a40;
        --accent: #4dabf7;
    }
}

body {
    background: var(--bg);
    color: var(--text);
}
  

Use CSS custom properties for all theme-dependent values — one media query switches the entire theme.

The color-scheme Property

Tell the browser to style native UI (scrollbars, form controls) for dark mode:

  :root {
    color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
    :root {
        color-scheme: dark;
    }
}
  

Without this, <input> and <select> may stay light-themed in dark pages.

Manual Theme Toggle

Support user override of system preference with a data-theme attribute:

  :root,
[data-theme="light"] {
    --bg: #ffffff;
    --text: #212529;
}

[data-theme="dark"] {
    --bg: #1a1a2e;
    --text: #e9ecef;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #1a1a2e;
        --text: #e9ecef;
    }
}
  
  const toggle = document.querySelector('#theme-toggle');
const stored = localStorage.getItem('theme');

if (stored) {
    document.documentElement.setAttribute('data-theme', stored);
}

toggle.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';
    document.documentElement.setAttribute('data-theme', next);
    localStorage.setItem('theme', next);
});
  

Prevent Flash of Wrong Theme (FOUT)

Inline script in <head> before CSS to apply stored theme immediately:

  <script>
(function() {
    const theme = localStorage.getItem('theme');
    if (theme) document.documentElement.setAttribute('data-theme', theme);
})();
</script>
  

Meta Theme Color

Update mobile browser chrome per theme:

  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1a1a2e" media="(prefers-color-scheme: dark)">
  

Images and Media in Dark Mode

  @media (prefers-color-scheme: dark) {
    .logo { content: url('/logo-dark.svg'); }
    img:not([src*=".svg"]) {
        opacity: 0.9;
    }
}
  

Or provide separate assets:

  <picture>
    <source srcset="diagram-dark.svg" media="(prefers-color-scheme: dark)">
    <img src="diagram-light.svg" alt="Architecture diagram">
</picture>
  

Shadows and Borders in Dark Mode

Pure black shadows disappear on dark backgrounds — use lighter, subtle borders:

  [data-theme="dark"] .card {
    background: #16213e;
    border: 1px solid #343a40;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
  

Contrast and Accessibility

Dark mode is not just inverted colors — maintain WCAG contrast ratios:

  • Body text: 4.5:1 minimum against background
  • Muted text: don’t go too dim (#6c757d on #1a1a2e may fail)
  • Test with WebAIM Contrast Checker

Avoid pure #000 background with pure #fff text — use off-white (#e9ecef) and dark gray (#1a1a2e) to reduce halation.

prefers-reduced-motion

Pair dark mode with motion preferences:

  @media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}
  

Design Token Structure

  :root {
    --color-bg-primary: #ffffff;
    --color-bg-secondary: #f8f9fa;
    --color-text-primary: #212529;
    --color-text-secondary: #6c757d;
    --color-accent: #007bff;
}

[data-theme="dark"] {
    --color-bg-primary: #1a1a2e;
    --color-bg-secondary: #16213e;
    --color-text-primary: #e9ecef;
    --color-text-secondary: #adb5bd;
    --color-accent: #4dabf7;
}
  

Semantic token names (bg-primary) survive theme changes without renaming.

Best Practices

  1. Default to system preference — don’t force dark mode
  2. Persist manual choice in localStorage
  3. Use semantic CSS variables — never hardcode colors in components
  4. Set color-scheme for native form controls
  5. Test both themes in CI or design review

Troubleshooting

Toggle doesn’t override system theme

  • Ensure [data-theme="dark"] selector specificity beats media query
  • Use :root:not([data-theme="light"]) pattern for system fallback

Flash of light theme on load

  • Add inline script in <head> before body renders

Images look wrong in dark mode

  • Provide dark variants or adjust opacity/borders

Dark mode is a user experience expectation — implement it with CSS variables, system detection, and accessible contrast.