What is CSS Custom Properties (Variables)?
CSS Custom Properties are author-defined values declared with `--` syntax that enable reusable, dynamic styling in stylesheets.
What is CSS Custom Properties (Variables)?
CSS Custom Properties — commonly called CSS Variables — are entities defined by CSS authors using the -- prefix that store reusable values within the cascade. Introduced in the CSS Custom Properties for Cascading Variables Module Level 1 spec (W3C Candidate Recommendation, first published 2015, broadly supported in browsers since 2017), they let you define a value once and reference it anywhere via var(). Unlike preprocessor variables from Sass or Less, custom properties are live in the browser: they participate in the cascade, can be inherited by child elements, and can be updated at runtime with JavaScript. As of April 2026, global browser support sits above 97% per caniuse.com. A common use case is theming — toggling between light and dark modes by swapping a handful of property values on :root or a parent selector.
How it works
You declare a custom property on any selector. The :root pseudo-class is the conventional place for global values:
:root {
--color-primary: #2563eb;
--spacing-md: 1rem;
--font-body: 'Inter', system-ui, sans-serif;
}
.card {
background: var(--color-primary);
padding: var(--spacing-md);
font-family: var(--font-body);
}
Key mechanics:
- Cascade and inheritance: Custom properties follow normal CSS cascade rules. A property set on
.theme-darkoverrides the same property on:rootfor that subtree. This is the core trick behind scoped theming. - Fallback values:
var(--color-accent, #f59e0b)returns the fallback if--color-accentis undefined. - Runtime updates via JS:
document.documentElement.style.setProperty('--color-primary', '#dc2626')instantly repaints every element referencing that variable. No class toggling, no stylesheet swaps. - Invalid at computed-value time: If a custom property resolves to an invalid value for the property it's used in (e.g.,
color: var(--spacing-md)where the value is1rem), the property falls back to its inherited or initial value — not to thevar()fallback. This trips people up.
Custom properties can reference other custom properties: --color-surface: hsl(var(--hue), 90%, 95%). This composability is what makes them powerful for design token systems.
When to use it
Use custom properties when:
- You're building a theming system (light/dark mode, white-label products). We've shipped this on 50+ projects and it's the single most effective pattern for multi-theme sites.
- You need runtime style changes driven by user interaction or JS state — sliders controlling layout gaps, dynamic accent colors, etc.
- You want a single source of truth for design tokens that CSS, JS, and frameworks can all read.
- You're working with component-scoped styles in frameworks like Astro or web components where Sass variables can't cross shadow DOM boundaries but inherited custom properties can.
Skip them when:
- You need compile-time logic (loops, conditionals, mixins). Preprocessors still own that space.
- You're targeting legacy IE11 environments (support is zero there).
- The value never changes and is used once — a plain value is simpler.
CSS Custom Properties vs alternatives
| Feature | CSS Custom Properties | Sass/Less Variables | Design Tokens (e.g., Style Dictionary) |
|---|---|---|---|
| Runtime update | ✅ Yes | ❌ Compiled away | Depends on output format |
| Cascade/inheritance | ✅ Yes | ❌ No | N/A — generates variables |
| Browser support | 97%+ | N/A (preprocessor) | N/A |
| Shadow DOM penetration | ✅ Via inheritance | ❌ No | Depends on output |
| Conditional logic | ❌ Limited | ✅ Mixins, loops | ✅ Build-time transforms |
| Tooling required | None | Sass compiler | Build pipeline (Style Dictionary, Tokens Studio) |
Our preferred stack: define design tokens in a tool like Tokens Studio, export them as CSS custom properties via Style Dictionary, and consume them in Tailwind CSS v4's @theme directive or plain CSS. This gives you the best of all three columns.
Real-world example
On a recent multi-brand e-commerce project in Astro 5, we defined 38 design tokens as custom properties on :root. Each brand override was a single CSS file swapping those 38 values — colors, spacing scale, border radii, and font stacks. Theme switching happened via a data-brand attribute on <html>, with selectors like [data-brand='acme'] redefining the properties. Total added CSS: ~1.2 KB uncompressed per brand. Dark mode was an additional layer using prefers-color-scheme and a manual toggle that called setProperty() in 4 lines of JS. Lighthouse performance scores stayed above 95 because there's zero layout shift — the browser repaints in a single frame when properties change.