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
- One token per concept. No
--border-2, ever. - Dark mode is a variable swap, not a redesign. If it needs layout changes, that's a separate concern.
- 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.