devnotes.

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.