~/TechPurAI
~/tutorials/css-from-scratch/dark-mode-and-prefers-color-scheme
intermediate·part 19 of 22·3 min read

Dark mode: real prefers-color-scheme support done properly

Updated Aug 16, 2026CSS

Part 5 built Bright Leaf Coffee's real design tokens as CSS custom properties. That decision pays off directly here — real dark mode support becomes a matter of redefining a handful of token values, not rewriting every individual rule that references a color.

The real, token-based approach

css
:root {
  --color-bg: #faf7f2;
  --color-panel: #ffffff;
  --color-ink: #2b2b2b;
  --color-muted: #6b6b6b;
  --color-line: #e5e0d8;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1a1714;
    --color-panel: #26221d;
    --color-ink: #f0ebe3;
    --color-muted: #a89f92;
    --color-line: #3a352e;
  }
}

Every other rule in Bright Leaf Coffee's stylesheet already references var(--color-bg), var(--color-ink), and so on, from part 5 onward — this real media query is the only place dark mode needs to be defined, since redefining the custom properties automatically cascades the new values to every single rule using them, with zero other changes required anywhere else in the stylesheet.

Why it matters

This is the real, practical payoff of building on design tokens from the start rather than writing raw color values directly into individual rules throughout the stylesheet. Retrofitting dark mode onto a real stylesheet full of hardcoded hex colors means hunting down and duplicating every single one inside a media query — a genuinely large, error-prone undertaking compared to redefining five token values once.

prefers-color-scheme: a real, genuine user preference, not a guess

css
@media (prefers-color-scheme: dark) {
  /* applies automatically when the visitor's OS is set to dark mode */
}

This media feature reflects a real, system-level setting most modern operating systems expose — respecting it automatically means Bright Leaf Coffee's site matches a visitor's actual stated preference the moment they arrive, with zero interaction required, rather than defaulting to light mode for everyone and requiring a manual toggle just to reach a preference they'd already expressed at the OS level.

Adding a real, manual toggle that overrides the system preference

css
:root {
  --color-bg: #faf7f2;
  --color-ink: #2b2b2b;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1a1714;
    --color-ink: #f0ebe3;
  }
}

/* a real, explicit override, regardless of system preference */
:root[data-theme="dark"] {
  --color-bg: #1a1714;
  --color-ink: #f0ebe3;
}

:root[data-theme="light"] {
  --color-bg: #faf7f2;
  --color-ink: #2b2b2b;
}

A real toggle button (implemented in JavaScript, outside this series' scope, setting a data-theme attribute on <html>) needs its explicit choice to win over the system preference in both directions — a visitor with a dark-mode OS who explicitly picks light mode on the site shouldn't have that choice silently overridden. The real, correct CSS ordering above achieves exactly that: the [data-theme] attribute selectors, being more specific than the bare :root inside a media query, take priority whenever the attribute is actually present, while the media query alone still governs the real, default behavior for a visitor who's never interacted with the toggle at all.

Testing real dark mode honestly

text
Browser dev tools: rendering → emulate CSS media feature
  prefers-color-scheme — genuinely useful for a fast first check
Actual OS setting: toggling the real system dark mode setting and
  reloading — the only way to catch a real discrepancy between the
  emulated dev-tools behavior and actual OS-level behavior

The same real testing discipline from part 14's responsive design coverage applies here — dev tools emulation is a fast, useful first pass, but genuinely verifying dark mode means actually toggling the real OS setting at least once before considering it done.

Common mistake

Defining dark mode colors only for backgrounds and text, while forgetting genuine contrast checks on interactive elements — a button or link that reads clearly in light mode can become genuinely low-contrast or invisible against a dark background if its own color wasn't deliberately redefined as part of the same token set. Every token category from part 5 — not just background and text — needs a real, deliberate dark-mode counterpart, not an assumption that it'll "just work."

Next: CSS architecture — naming conventions, avoiding specificity wars, and organizing a real stylesheet so it stays maintainable as it grows well past the size this series has built so far.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous18. CSS and Core Web Vitals: render-blocking CSS, critical CSS, and layout shiftnext →20. CSS architecture: organizing a real stylesheet at scale