How do I add dark mode to a website?

Short answer

Put every colour in a custom property, redefine those properties inside @media (prefers-color-scheme: dark), and add a data-theme attribute for a manual toggle.

#1. Colours become variables

css
:root {
  --page: #f4f9ff;
  --surface: #ffffff;
  --ink: #0c1c30;
  --ink-muted: #546a92;
  --line: #d7e7fa;
}

Components only ever reference the variables:

css
.card {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
}

#2. Redefine them for dark

css
:root[data-theme='dark'] {
  --page: #070e1b;
  --surface: #0f1c30;
  --ink: #e9f2ff;
  --ink-muted: #8fa8ca;
  --line: #1f3454;
}

Not one component rule changes. This is the whole argument for custom properties.

#3. Respect the OS, allow an override

html
<script>
(function () {
  try {
    var stored = localStorage.getItem('theme');
    var prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
    document.documentElement.dataset.theme = stored || (prefersDark ? 'dark' : 'light');
  } catch (e) {}
})();
</script>

Put this inline in <head>, before the stylesheet renders. An external or deferred script runs after first paint, which produces a white flash on every page load.

#Things that catch people out

Do not use pure black. #000 under white text causes halation that makes reading tiring. A very dark desaturated blue reads as black and is far more comfortable.

Pull white text back too. Pure white on dark is usually too bright; tint it slightly toward the background hue.

Shadows need rethinking. A soft grey shadow is invisible on a dark surface. Use a darker, more opaque shadow, or switch to a subtle border.

Images may need it. Screenshots of light UIs look like holes in a dark page. filter: brightness(.85) helps, or ship both.

Tell the browser. <meta name="color-scheme" content="light dark"> makes form controls and scrollbars match.