← All articles
CSS Sep 28, 2026 6 min read

Theming with CSS custom properties

Dark mode used to mean maintaining a second stylesheet. With custom properties, it means redefining twelve variables under one selector. That's the whole trick — here's how to do it well.

Name for the role, not the value

--brand-purple-500 tells you what the color is. --accent tells you what it does. Role names survive rebranding; value names don't.

:root {
  --bg: #f7f8fc;          /* role: page background   */
  --text-soft: #5a6478;   /* role: secondary text    */
  --accent: #5b5ef4;      /* role: interactive color */
}

[data-theme="dark"] {
  --bg: #0f1220;
  --text-soft: #a4abc4;
  --accent: #8b8dff;      /* brighter for contrast on dark */
}

Derive, don't duplicate

Derived values — soft tints, shadows, borders — should be computed from your base tokens so a single change ripples through the theme:

:root {
  --accent: #5b5ef4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);
}

Note the dark theme keeps the same formula — only --accent changes, and every tint, border, and gradient updates automatically.

Three rules that prevent theme sprawl

  1. One token per concept. No --border-2, ever.
  2. Dark mode is a variable swap, not a redesign. If it needs layout changes, that's a separate concern.
  3. Respect the system first. Default to prefers-color-scheme, then let an explicit user choice override and persist it.

Do this and theming becomes a feature you ship once and forget — which is exactly how this site's toggle works. Try it in the footer.