Theming with CSS Variables
Because CSS custom properties can be redefined per-scope and read live by JavaScript, they're the standard modern technique for building light/dark themes and design systems.
A simple two-theme setup:
:root {
--bg-color: #ffffff;
--text-color: #111111;
--card-bg: #f5f5f5;
}
[data-theme="dark"] {
--bg-color: #111111;
--text-color: #f5f5f5;
--card-bg: #222222;
}
body {
background: var(--bg-color);
color: var(--text-color);
transition: background 0.3s, color 0.3s;
}
.card {
background: var(--card-bg);
}
<html data-theme="dark">
...
</html>
Every element referencing var(--bg-color) or var(--text-color) updates automatically the instant data-theme changes — no need to rewrite dozens of individual color rules, since the components themselves never change, only the variable values they read from.
Toggling the theme with a tiny bit of JavaScript:
<script>
function toggleTheme() {
const html = document.documentElement;
const isDark = html.getAttribute("data-theme") === "dark";
html.setAttribute("data-theme", isDark ? "light" : "dark");
}
</script>
Combining with prefers-color-scheme to respect the OS setting by default, while still allowing a manual override:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg-color: #111111;
--text-color: #f5f5f5;
}
}
This pattern — variables at :root, overridden per data-attribute or media query — scales cleanly from a simple two-color toggle to a full design system with dozens of tokens for spacing, radius, shadows, and typography, all switchable from one place.