CSS Custom Properties (CSS variables) have matured far beyond simple color swapping. In 2026, they power dynamic theming systems, component state management, calculated layouts, and real-time JavaScript interop. This guide explores the advanced patterns that make CSS Custom Properties one of the most powerful features in modern CSS.
Custom Properties Fundamentals
Custom properties are defined with a -- prefix and accessed with the var() function. They cascade, inherit, and can be overridden at any scope — making them fundamentally different from preprocessor variables like SASS $variables.
/* CSS Custom Properties — Fundamentals */
/* Define properties on :root for global scope */
:root {
--color-primary: #0066cc;
--color-primary-dark: #0052a3;
--spacing-unit: 8px;
--font-size-base: 1rem;
--border-radius: 4px;
}
/* Use with var() */
.button {
background-color: var(--color-primary);
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
border-radius: var(--border-radius);
font-size: var(--font-size-base);
}
/* Override at component scope (not global) */
.button.danger {
--color-primary: #cc0000; /* Only affects this button */
--color-primary-dark: #990000;
}
/* Fallback value (second argument to var) */
.card {
background: var(--card-bg, white);
color: var(--card-text, var(--color-text, #333)); /* nested fallback */
padding: var(--card-padding, var(--spacing-unit, 8px));
}Design Token Theming Systems
The most powerful use of custom properties is building a complete design token system that supports multiple themes with zero JavaScript.
/* Multi-Theme System with CSS Custom Properties */
/* Light theme (default) */
:root {
--color-bg: #ffffff;
--color-surface: #f8f9fa;
--color-border: #e2e8f0;
--color-text-primary: #1a202c;
--color-text-secondary: #718096;
--color-accent: #3182ce;
--color-accent-hover: #2c5282;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition-fast: 150ms ease;
}
/* Dark theme — override only what changes */
[data-theme="dark"] {
--color-bg: #1a202c;
--color-surface: #2d3748;
--color-border: #4a5568;
--color-text-primary: #f7fafc;
--color-text-secondary: #a0aec0;
--color-accent: #63b3ed;
--color-accent-hover: #90cdf4;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
}
/* High-contrast theme */
[data-theme="high-contrast"] {
--color-bg: #000000;
--color-text-primary: #ffffff;
--color-accent: #ffff00;
--color-border: #ffffff;
}
/* System preference auto-detection */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg: #1a202c;
--color-text-primary: #f7fafc;
/* ... more overrides */
}
}
/* Components use semantic tokens — never raw colors */
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
color: var(--color-text-primary);
box-shadow: var(--shadow-md);
transition: box-shadow var(--transition-fast);
}Component Variants with Custom Properties
Custom properties enable component variant systems that are more flexible than traditional utility class approaches. The component defines sensible defaults; callers override them at the component scope.
/* Component Variant System */
/* Button component with custom property API */
.btn {
/* Default values (the component's "API") */
--btn-bg: var(--color-accent);
--btn-bg-hover: var(--color-accent-hover);
--btn-color: white;
--btn-border: transparent;
--btn-shadow: var(--shadow-sm);
--btn-size: 1rem;
--btn-padding-y: 0.5em;
--btn-padding-x: 1em;
--btn-radius: var(--border-radius);
background: var(--btn-bg);
color: var(--btn-color);
border: 1px solid var(--btn-border);
box-shadow: var(--btn-shadow);
font-size: var(--btn-size);
padding: var(--btn-padding-y) var(--btn-padding-x);
border-radius: var(--btn-radius);
transition: background var(--transition-fast);
}
.btn:hover {
background: var(--btn-bg-hover);
}
/* Variants — only override what changes */
.btn-danger {
--btn-bg: #e53e3e;
--btn-bg-hover: #c53030;
}
.btn-outline {
--btn-bg: transparent;
--btn-bg-hover: var(--color-accent);
--btn-color: var(--color-accent);
--btn-border: var(--color-accent);
}
.btn-sm {
--btn-size: 0.875rem;
--btn-padding-y: 0.375em;
--btn-padding-x: 0.75em;
}
.btn-lg {
--btn-size: 1.125rem;
--btn-padding-y: 0.75em;
--btn-padding-x: 1.5em;
}
/* Caller overrides — no need for a new variant class */
.special-hero .btn {
--btn-bg: gold;
--btn-color: black;
--btn-radius: 50px;
}JavaScript Interoperability
Custom properties bridge CSS and JavaScript seamlessly. You can read and write them from JavaScript, enabling dynamic animations, real-time updates, and reactive UIs without class toggling.
// JavaScript <-> CSS Custom Properties
// 1. Read a custom property value
const root = document.documentElement;
const primaryColor = getComputedStyle(root)
.getPropertyValue('--color-primary')
.trim();
console.log(primaryColor); // '#0066cc'
// 2. Set a custom property from JavaScript
root.style.setProperty('--color-primary', '#ff6600');
// 3. Remove a custom property
root.style.removeProperty('--color-primary');
// 4. Theme switcher
function setTheme(theme) {
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
}
// 5. Animate with custom properties (requires @property)
// CSS:
// @property --progress {
// syntax: '<number>';
// initial-value: 0;
// inherits: false;
// }
// .progress-bar {
// width: calc(var(--progress) * 1%);
// transition: --progress 1s ease;
// }
// JavaScript:
function animateProgress(el, from, to) {
el.style.setProperty('--progress', from);
requestAnimationFrame(() => {
el.style.setProperty('--progress', to);
});
}
// 6. Reactive properties with ResizeObserver
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
entry.target.style.setProperty('--el-width', `${width}px`);
entry.target.style.setProperty('--el-height', `${height}px`);
}
});
observer.observe(document.querySelector('.responsive-component'));Frequently Asked Questions
Are CSS Custom Properties the same as SASS variables?
No, they are fundamentally different. SASS variables are compiled at build time and produce static CSS. CSS Custom Properties exist at runtime in the browser, cascade through the DOM like any CSS property, can be changed with JavaScript, and can be inspected in DevTools.
Can CSS Custom Properties be animated?
Yes, when used with @property (Houdini). The @property rule lets you declare custom properties with a specific type (color, length, integer, etc.), enabling smooth animations. Without @property, custom properties animate by switching values at the start/end, not interpolating.
What is the fallback value in var()?
The second argument to var() is a fallback used when the property is not defined or invalid. Example: var(--color-primary, #0066cc) will use #0066cc if --color-primary is not set. You can nest var() in fallbacks: var(--spacing-custom, var(--spacing-base, 16px)).
Do CSS Custom Properties work in all modern browsers?
Yes, CSS Custom Properties have full browser support since 2017 (Chrome 49+, Firefox 31+, Safari 9.1+). The @property Houdini rule has slightly less support but works in all major browsers as of 2024.