Dark Mode

`prefers-color-scheme` and a Token-Based Approach

Dark Mode

Respect the user's system preference with `prefers-color-scheme`, plus an optional manual toggle.

4 min read Level 2/5 #css#dark-mode#color-scheme
What you'll learn
  • Respect the user's OS preference
  • Build a `data-theme` toggle
  • Use `color-scheme` for native form controls

A modern site supports both light and dark themes. Two pieces — respect the user’s OS setting, and offer a manual toggle.

OS Preference

:root {
  --bg: white;
  --text: #111;
  --brand: #6360ff;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #111;
    --text: #f4f4f5;
    --brand: #918eff;
  }
}

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

When the OS is in dark mode, the variables flip. Everything that uses them re-themes.

Manual Toggle

For a user-controlled toggle, attach the theme to the document via a data-theme attribute:

:root {
  --bg: white;
  --text: #111;
}

:root[data-theme="dark"] {
  --bg: #111;
  --text: #f4f4f5;
}

JS sets the attribute:

document.documentElement.dataset.theme = "dark";

To honor BOTH OS preference and a manual override:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #111;
    --text: #f4f4f5;
  }
}

:root[data-theme="dark"] {
  --bg: #111;
  --text: #f4f4f5;
}

color-scheme — Native Controls

:root { color-scheme: light dark; }

Tells the browser the page supports both — native scrollbars, form controls, and selection colors adapt.

For just dark:

[data-theme="dark"] {
  color-scheme: dark;
}

Avoid the Flash

A naïve toggle reads JS BEFORE applying — meaning the page flashes in light mode before going dark. Move the theme decision to a small inline <script> at the top of <head>, BEFORE the CSS loads:

<script>
  const t = localStorage.getItem("theme") ||
    (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
  document.documentElement.dataset.theme = t;
</script>

Up Next

Scroll snap for carousels and slide decks.

Scroll Snap →