Using CSS Custom Properties as Runtime Design Tokens for Light/Dark Themes
Learn how CSS custom properties let you change colors, spacing, and other design tokens at runtime—no stylesheet swap or page reload required.
28 May 2026, 21:57 UTC

Problem: switching themes without a reload
When a site offers a light and a dark mode, the naïve approach is to load two separate stylesheets or to replace a block of CSS rules via JavaScript. Both methods force the browser to repaint the whole page and can cause a flash of unstyled content. A lighter‑weight alternative is to keep a single stylesheet and let the browser recompute only the values that actually change.
How custom properties work as design tokens
CSS custom properties (often called CSS variables) are declared with a double‑dash prefix, e.g. --color-bg. They follow the normal cascade and inheritance rules: a value set on an ancestor element is inherited by its descendants unless overridden. The var() function retrieves the current computed value, and it accepts a fallback that is used when the property is not defined or when the browser does not understand custom properties.
Worked example: light/dark theme switch
:root {
--color-bg: #fff; /* light mode background */
--color-fg: #111; /* light mode foreground */
}
.dark {
--color-bg: #111; /* dark mode background */
--color-fg: #eee; /* dark mode foreground */
}
body {
background: var(--color-bg, #fff);
color: var(--color-fg, #111);
}
To toggle the theme, add or remove the .dark class on a high‑level element such as <html> or <body>. Because the custom properties are inherited, every element that uses var(--color-bg) or var(--color-fg) updates instantly.
Trade‑offs versus Sass variables
- Runtime vs compile time: Sass variables are resolved to static values during the build step, so changing a theme requires serving a different CSS file. Custom properties stay live in the browser, allowing a single stylesheet to support many themes.
- Fallback handling: With Sass you must generate separate files or rely on server‑side logic. Custom properties let you write a static fallback (
var(--token, fallback)) that works in browsers that lack support while still enabling the feature where it is available. - File size: A single stylesheet with custom properties is usually smaller than maintaining multiple theme‑specific stylesheets.
Limitations and practical verification
Custom properties are not a panacea. Keep these points in mind:
- Cannot be used in query conditions: Values inside
@mediaor@containerare resolved before custom properties are computed, so@media (--color-bg: #fff)does not work. You must keep the query literal and set the token inside the block. - Type safety: By default a custom property is an untyped string. Assigning an invalid value (e.g.
--color-bg: 20pxto a property that expects a color) will cause the consuming property to fall back to its initial or inherited value. Registering the property with@propertyadds type checking, but that feature is still limited to newer browsers. - Performance impact: Changing many tokens on a large DOM triggers a style recalculation for all affected elements. For modest numbers of tokens (a handful of colors, spacings, radii) the cost is negligible, but on very large pages you should profile with the browser’s Performance panel.
How to verify that the mechanism works as expected:
- Open DevTools, select an element that uses
var(--color-bg), and view the Computed pane. The resolved value should match the token defined on the nearest ancestor. - Toggle the
.darkclass on<html>in the Elements pane and watch the computed background and foreground colors change without a page reload. - In the Performance panel, record a short session while toggling the theme class. Look for a “Recalculate Style” event; its duration should be low compared to a full stylesheet swap.
Actionable checklist
- Define your design tokens on
:rootor a theme wrapper class. - Consume them with
var(--token, fallback)to provide a static fallback for older browsers. - Switch themes by adding/removing a class that redefines the tokens; avoid setting tokens deep inside the component tree unless you intentionally want scoping.
- Never place
var()inside a media query condition; keep the query literal and set the token inside the block. - If you need type safety, experiment with
@propertyregistration and check browser support before relying on it. - After deploying, use DevTools to confirm computed values and the Performance panel to ensure style recalculation stays within your budget.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.