CSS Custom Properties: The Complete Guide to CSS Variables
Master CSS custom properties from basics to advanced patterns. Theming, dynamic updates with JavaScript, scoped variables, fallback values and real design system examples.
CSS custom properties — colloquially called CSS variables — are one of the most transformative features added to CSS in years. They're not just a convenience. They fundamentally change how you architect styles.
Custom properties are defined with a double-dash prefix and read with var():
/* Define on :root to make globally available */
:root {
--brand-color: #e8a020;
--font-size-base: 16px;
--spacing-md: 1.5rem;
}
/* Use anywhere */
.button {
background: var(--brand-color);
font-size: var(--font-size-base);
padding: var(--spacing-md);
}
Unlike Sass variables (which are compiled away), CSS custom properties exist at runtime. They can be changed by JavaScript, overridden by media queries, and scoped to elements.
var() accepts a fallback as a second argument:
.card {
/* Uses --card-bg if defined, otherwise falls back to white */
background: var(--card-bg, #ffffff);
/* Fallbacks can themselves use var() */
color: var(--text-primary, var(--fallback-text, #333333));
}
Custom properties make theming trivial. Define all your theme values on :root for the default theme, then override on a [data-theme] attribute:
:root {
--bg: #ffffff;
--text: #1a1a1a;
--accent: #e8a020;
}
[data-theme="dark"] {
--bg: #0d0d0d;
--text: #f0ede8;
--accent: #f0b030;
}
body {
background: var(--bg);
color: var(--text);
}
Every element using these variables automatically updates. No JavaScript needed to restyle components — just toggle the attribute.
This is where CSS variables truly separate from Sass. You can read and write them at runtime:
// Read a CSS variable
const accent = getComputedStyle(document.documentElement)
.getPropertyValue('--accent').trim();
// Write a CSS variable (instantly updates all consumers)
document.documentElement.style.setProperty('--accent', '#ff6b6b');
// Scoped to a specific element
const card = document.querySelector('.card');
card.style.setProperty('--card-opacity', '0.5');
This enables live theming sliders, per-element customisation and dynamic values driven by user interaction — without touching class names or inline styles on individual elements.
At scale, custom properties become your design token layer:
:root {
/* Primitives — raw values */
--color-amber-500: #e8a020;
--color-slate-900: #0d0d0d;
--space-4: 1rem;
--space-8: 2rem;
/* Semantic tokens — intent-based */
--color-background: var(--color-slate-900);
--color-surface: #181818;
--color-text: #f0ede8;
--color-interactive: var(--color-amber-500);
/* Component tokens — component-scoped */
--button-bg: var(--color-interactive);
--button-text: var(--color-slate-900);
--button-radius: 8px;
}
This three-layer approach is used by every major design system — Figma Tokens, Radix, Shadcn, and more. The semantic layer is what makes theming surgical rather than a global find-and-replace.