CSS Custom Properties (Variables): The Complete Guide for 2026
CSS Custom Properties, commonly called CSS Variables, are one of the most impactful additions to CSS in the past decade. They let you define reusable values directly in your stylesheets, update them dynamically with JavaScript, scope them to specific parts of your DOM, and build flexible theming systems that were previously impossible without preprocessors or build tools. If you have been relying on Sass variables, hardcoded color values, or JavaScript-driven inline styles for theming, CSS custom properties replace all of that with a native, runtime-capable, cascading solution.
This guide covers everything you need to know about CSS custom properties in production: the syntax for declaration and usage, how scope and inheritance work, fallback values and nested variables, dynamic theming with dark mode, JavaScript integration, advanced patterns for responsive design and component architecture, performance considerations, and browser support. Every concept includes code you can copy and use immediately.
What Are CSS Custom Properties?
CSS Custom Properties are entities defined by CSS authors that contain specific values to be reused throughout a document. They are set using the -- prefix notation and accessed using the var() function. Unlike preprocessor variables (Sass $variables, Less @variables), CSS custom properties are part of the live CSS Object Model. They exist at runtime, participate in the cascade, inherit through the DOM tree, and can be read and modified with JavaScript.
Here is the simplest possible example:
:root {
--primary-color: #3b82f6;
--spacing-md: 1rem;
}
.button {
background-color: var(--primary-color);
padding: var(--spacing-md);
}
The :root pseudo-class selector targets the root element of the document (the <html> element in HTML), making these variables available everywhere in the document. The var() function retrieves the current value of the custom property. That is the entire mechanism: define with --name, use with var(--name).
What makes custom properties fundamentally different from preprocessor variables is that they are live. When you change a custom property value (via a class change, a media query, or JavaScript), every element using that property updates automatically. Preprocessor variables are resolved at compile time and produce static CSS. Custom properties are resolved at runtime and respond to the current state of the document.
Syntax and Declaration
CSS custom properties follow specific syntax rules that are important to understand before using them in production.
Naming Rules
Custom property names must start with two dashes (--) followed by an identifier. They are case-sensitive, so --primary-color and --Primary-Color are two different properties.
/* Valid custom property names */
--color: blue;
--my-font-size: 16px;
--spacing-lg: 2rem;
--123: "numbers are fine after the dashes";
--_private: hidden;
--PRIMARY: #ff0000;
/* Invalid — does not start with -- */
-color: blue; /* This is a vendor prefix, not a custom property */
color: blue; /* This is a standard property */
$color: blue; /* This is Sass syntax, not valid CSS */
Convention favors lowercase with hyphens: --primary-color, --font-size-base, --border-radius-sm. Many teams prefix custom properties with a namespace to avoid collisions in large projects: --dt-color-primary, --btn-padding, --card-shadow.
Declaring on :root (Global Variables)
The most common pattern is declaring custom properties on :root to make them globally available:
:root {
/* Colors */
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
/* Typography */
--font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.5rem;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Layout */
--border-radius: 8px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--max-width: 1200px;
--transition-speed: 200ms;
}
This creates a centralized design token system. Change --color-primary once, and every button, link, heading, and accent element across the entire site updates. This is the same concept as design tokens in tools like Figma or Style Dictionary, but implemented natively in CSS.
Using var() to Access Values
The var() function is the only way to reference a custom property value:
.card {
background: var(--color-surface);
border-radius: var(--border-radius);
padding: var(--spacing-lg);
box-shadow: var(--shadow-md);
font-family: var(--font-family-base);
}
.button-primary {
background: var(--color-primary);
color: white;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
transition: background var(--transition-speed) ease;
}
/* Custom properties work inside any CSS value, including shorthand */
.hero {
margin: var(--spacing-xl) auto;
border: 2px solid var(--color-primary);
font: var(--font-size-lg) / 1.6 var(--font-family-base);
}
Scope and Inheritance
The most powerful aspect of CSS custom properties is that they follow the same cascading and inheritance rules as every other CSS property. This means you can define global defaults and override them locally for specific components or sections of the page.
Global Scope (:root)
Properties declared on :root are inherited by every element in the document, because :root is the top of the inheritance tree:
:root {
--text-color: #e4e4e7;
--bg-color: #0f1117;
}
/* Every element inherits these values */
body {
color: var(--text-color);
background: var(--bg-color);
}
Local Scope (Component-Level)
You can declare custom properties on any element, and they will only be available to that element and its descendants:
/* Global default */
:root {
--text-color: #e4e4e7;
--accent: #3b82f6;
}
/* Override for a specific section */
.sidebar {
--accent: #8b5cf6; /* Purple accent in sidebar */
}
.footer {
--accent: #10b981; /* Green accent in footer */
}
/* This rule uses whichever --accent is in scope */
.link {
color: var(--accent);
}
/* Result:
- Links in the sidebar are purple
- Links in the footer are green
- Links everywhere else are blue */
This is the foundation of component theming. You define a component that uses custom properties for its configurable values, then override those properties in different contexts without modifying the component's CSS.
Inheritance Through the DOM
Custom properties inherit through the DOM tree, not through CSS selectors. If an element does not have a custom property defined on it, it inherits the value from its parent, which inherits from its parent, all the way up to :root.
<div class="outer"> <!-- --gap: 2rem -->
<div class="middle"> <!-- inherits --gap: 2rem -->
<div class="inner"> <!-- --gap: 0.5rem (overridden) -->
<p>Content</p> <!-- inherits --gap: 0.5rem -->
</div>
</div>
</div>
.outer {
--gap: 2rem;
}
.inner {
--gap: 0.5rem; /* Overrides the inherited value */
}
/* The <p> inside .inner sees --gap as 0.5rem
The <div class="middle"> sees --gap as 2rem */
Specificity and the Cascade
Custom properties participate in the normal CSS cascade. Higher specificity selectors and later declarations win, just like any other property:
:root {
--color: red;
}
.section {
--color: blue; /* More specific context */
}
#hero {
--color: green; /* Even more specific (ID selector) */
}
/* .section elements see blue, #hero sees green, everything else sees red */
Computed Values
An important detail: custom properties store their values as tokens and resolve them at computed-value time. This means that a custom property containing 10px + 5px does not compute to 15px automatically. You need calc() for arithmetic:
:root {
--base-size: 16px;
--scale: 1.25;
}
h1 {
/* This works: calc() resolves the arithmetic */
font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale));
/* Result: 16px * 1.25 * 1.25 * 1.25 = 31.25px */
}
h2 {
font-size: calc(var(--base-size) * var(--scale) * var(--scale));
/* Result: 16px * 1.25 * 1.25 = 25px */
}
h3 {
font-size: calc(var(--base-size) * var(--scale));
/* Result: 16px * 1.25 = 20px */
}
This pattern creates a modular type scale driven entirely by two custom properties. Change --base-size or --scale, and every heading size updates proportionally.
Fallback Values and Nested Variables
The var() function accepts an optional second argument: a fallback value that is used when the custom property is not defined or is invalid.
Basic Fallbacks
.card {
/* If --card-bg is not defined, use #1e2130 */
background: var(--card-bg, #1e2130);
/* If --card-radius is not defined, use 8px */
border-radius: var(--card-radius, 8px);
/* Fallback can be any valid CSS value, including complex ones */
box-shadow: var(--card-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));
}
Fallbacks are essential for building reusable components. They provide sensible defaults while allowing consumers to customize behavior by defining the expected custom properties.
Nested var() (Chained Fallbacks)
The fallback value itself can contain another var() reference, creating a fallback chain:
.element {
/* Try --override, then --theme-color, then fall back to #3b82f6 */
color: var(--override, var(--theme-color, #3b82f6));
/* Try component-specific, then global, then hardcoded */
padding: var(--card-padding, var(--spacing-md, 1rem));
}
This pattern is powerful for building layered configuration systems. A component first checks for a component-specific override, then a theme-level value, then a hardcoded default. It mirrors the pattern of environment-specific configuration in application development.
What Counts as "Not Defined"?
The fallback is used when the custom property is not set or not inherited by the element. However, if the property is set to an invalid value for the context where it is used, the behavior is different. CSS treats the property as valid at parse time (because custom properties accept any value) but invalid at computed-value time, which results in the guaranteed-invalid value behavior:
:root {
--not-a-color: "hello world";
}
.text {
/* --not-a-color is defined, so the fallback is NOT used.
But "hello world" is not a valid color.
The result: color reverts to the inherited value. */
color: var(--not-a-color, red);
}
/* To explicitly trigger the fallback, leave the property undefined
or set it to the keyword 'initial': */
:root {
--maybe-color: initial; /* This resets to guaranteed-invalid */
}
.text {
/* Now the fallback IS used, because initial means "not set" */
color: var(--maybe-color, red); /* Result: red */
}
This behavior is a common source of confusion. The rule is: fallbacks activate when the custom property is not in scope. If it is in scope but holds an invalid value for the property being set, the declaration becomes invalid at computed-value time and the property falls back to its inherited or initial value, not the var() fallback.
Dynamic Theming: Dark Mode and User Preferences
CSS custom properties are the standard approach for implementing dynamic themes in modern web applications. The pattern is to define all theme-dependent values as custom properties, then swap those values based on user preference or an explicit toggle.
Dark Mode with prefers-color-scheme
/* Light theme (default) */
:root {
--color-bg: #ffffff;
--color-surface: #f8f9fa;
--color-text: #1a1a2e;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Dark theme — triggered by system preference */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f1117;
--color-surface: #1a1d27;
--color-text: #e4e4e7;
--color-text-muted: #9ca3af;
--color-border: #2a2e3a;
--color-primary: #60a5fa;
--color-primary-hover: #93bbfd;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
}
/* Components use the variables — they work in both themes */
body {
background: var(--color-bg);
color: var(--color-text);
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
box-shadow: var(--shadow);
}
a {
color: var(--color-primary);
}
a:hover {
color: var(--color-primary-hover);
}
With this structure, every component that uses the custom properties adapts to the theme automatically. You never write theme-specific styles on individual components. All the theme logic is centralized in one place: the custom property declarations.
Manual Dark Mode Toggle with JavaScript
To let users override their system preference with a manual toggle:
/* Light theme (default) */
:root {
--color-bg: #ffffff;
--color-text: #1a1a2e;
/* ... other light values ... */
}
/* Dark theme via class */
:root.dark,
[data-theme="dark"] {
--color-bg: #0f1117;
--color-text: #e4e4e7;
/* ... other dark values ... */
}
/* Respect system preference when no manual choice */
@media (prefers-color-scheme: dark) {
:root:not(.light):not([data-theme="light"]) {
--color-bg: #0f1117;
--color-text: #e4e4e7;
}
}
// JavaScript toggle
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
// Restore saved preference on page load
const saved = localStorage.getItem('theme');
if (saved === 'dark') {
document.documentElement.classList.add('dark');
} else if (saved === 'light') {
document.documentElement.classList.remove('dark');
}
// If no saved preference, the system preference applies via CSS
Multiple Theme Support
Custom properties make it straightforward to support more than two themes:
/* Base/default theme */
:root {
--color-bg: #ffffff;
--color-primary: #3b82f6;
--color-accent: #8b5cf6;
}
/* Dark theme */
[data-theme="dark"] {
--color-bg: #0f1117;
--color-primary: #60a5fa;
--color-accent: #a78bfa;
}
/* High contrast theme */
[data-theme="high-contrast"] {
--color-bg: #000000;
--color-primary: #ffffff;
--color-accent: #ffff00;
--color-text: #ffffff;
--color-border: #ffffff;
}
/* Warm theme */
[data-theme="warm"] {
--color-bg: #fdf6e3;
--color-primary: #b58900;
--color-accent: #cb4b16;
--color-text: #073642;
}
// Switch themes with a single line
document.documentElement.setAttribute('data-theme', 'warm');
Using CSS Variables with JavaScript
CSS custom properties bridge the gap between CSS and JavaScript. You can read computed values, set new values, and build interactive interfaces where JavaScript controls the visual state through custom properties rather than inline styles or class toggling.
Reading Custom Property Values
// Read a custom property from the root element
const root = document.documentElement;
const styles = getComputedStyle(root);
const primaryColor = styles.getPropertyValue('--color-primary').trim();
console.log(primaryColor); // "#3b82f6"
const spacing = styles.getPropertyValue('--spacing-md').trim();
console.log(spacing); // "1rem"
// Read from a specific element (gets the computed/inherited value)
const card = document.querySelector('.card');
const cardBg = getComputedStyle(card).getPropertyValue('--card-bg').trim();
console.log(cardBg); // Whatever --card-bg resolves to for that element
Setting Custom Property Values
// Set a custom property on the root (affects the entire document)
document.documentElement.style.setProperty('--color-primary', '#ef4444');
// Set on a specific element (affects only that element and its descendants)
const sidebar = document.querySelector('.sidebar');
sidebar.style.setProperty('--sidebar-width', '300px');
// Remove a custom property (reverts to inherited/default value)
document.documentElement.style.removeProperty('--color-primary');
Practical Example: User-Controlled Theme Customizer
<div class="theme-controls">
<label>Primary Color:
<input type="color" id="primary-picker" value="#3b82f6">
</label>
<label>Border Radius:
<input type="range" id="radius-slider" min="0" max="24" value="8">
</label>
<label>Font Size:
<input type="range" id="font-slider" min="12" max="24" value="16">
</label>
</div>
const root = document.documentElement;
document.getElementById('primary-picker').addEventListener('input', (e) => {
root.style.setProperty('--color-primary', e.target.value);
});
document.getElementById('radius-slider').addEventListener('input', (e) => {
root.style.setProperty('--border-radius', e.target.value + 'px');
});
document.getElementById('font-slider').addEventListener('input', (e) => {
root.style.setProperty('--font-size-base', e.target.value + 'px');
});
// Save preferences
document.getElementById('primary-picker').addEventListener('change', (e) => {
localStorage.setItem('user-primary', e.target.value);
});
// Restore on load
const savedPrimary = localStorage.getItem('user-primary');
if (savedPrimary) {
root.style.setProperty('--color-primary', savedPrimary);
document.getElementById('primary-picker').value = savedPrimary;
}
This pattern is extremely powerful. With a handful of custom properties and a few event listeners, you get a fully interactive theme customizer. The CSS does not need to know about the JavaScript, and the JavaScript does not need to know about the CSS rules. They communicate through the shared interface of custom property names.
Reactive Animations with Mouse Position
/* CSS */
.spotlight {
--mouse-x: 50%;
--mouse-y: 50%;
background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
rgba(59, 130, 246, 0.15) 0%,
transparent 50%
);
transition: background 50ms ease;
}
// JavaScript
const spotlight = document.querySelector('.spotlight');
spotlight.addEventListener('mousemove', (e) => {
const rect = spotlight.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
spotlight.style.setProperty('--mouse-x', x + '%');
spotlight.style.setProperty('--mouse-y', y + '%');
});
This creates a spotlight effect that follows the cursor. The CSS handles the visual output, JavaScript handles the input. The custom properties are the bridge. This approach is significantly cleaner and more performant than generating inline background styles on every mouse move.
Advanced Patterns
With the fundamentals covered, here are the advanced patterns that experienced CSS developers use in production applications.
Responsive Design with Custom Properties
Custom properties can hold different values at different breakpoints, creating a responsive system that components consume without knowing anything about screen sizes:
:root {
--columns: 1;
--gap: 0.75rem;
--container-padding: 1rem;
--font-size-heading: 1.5rem;
}
@media (min-width: 640px) {
:root {
--columns: 2;
--gap: 1rem;
--container-padding: 1.5rem;
--font-size-heading: 1.75rem;
}
}
@media (min-width: 1024px) {
:root {
--columns: 3;
--gap: 1.5rem;
--container-padding: 2rem;
--font-size-heading: 2rem;
}
}
@media (min-width: 1280px) {
:root {
--columns: 4;
--gap: 2rem;
--container-padding: 3rem;
--font-size-heading: 2.5rem;
}
}
/* Components use the variables without media queries */
.card-grid {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: var(--gap);
padding: var(--container-padding);
}
.section-heading {
font-size: var(--font-size-heading);
}
This centralizes all responsive breakpoint logic in one place. Components become simpler because they do not need their own media queries. When you need to adjust the responsive behavior, you change the custom properties, not dozens of component styles.
Component Variants
Custom properties enable a clean pattern for component variants without duplicating entire rule sets:
/* Base button with configurable properties */
.btn {
--btn-bg: var(--color-primary);
--btn-color: #ffffff;
--btn-padding-x: 1rem;
--btn-padding-y: 0.5rem;
--btn-radius: var(--border-radius);
--btn-font-size: var(--font-size-base);
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--btn-bg);
color: var(--btn-color);
padding: var(--btn-padding-y) var(--btn-padding-x);
border-radius: var(--btn-radius);
font-size: var(--btn-font-size);
border: 2px solid var(--btn-bg);
cursor: pointer;
transition: all var(--transition-speed) ease;
}
.btn:hover {
filter: brightness(1.1);
}
/* Variants override only the custom properties */
.btn-secondary {
--btn-bg: transparent;
--btn-color: var(--color-primary);
}
.btn-danger {
--btn-bg: var(--color-danger);
}
.btn-success {
--btn-bg: var(--color-success);
}
/* Sizes override padding and font-size */
.btn-sm {
--btn-padding-x: 0.75rem;
--btn-padding-y: 0.25rem;
--btn-font-size: var(--font-size-sm);
}
.btn-lg {
--btn-padding-x: 1.5rem;
--btn-padding-y: 0.75rem;
--btn-font-size: var(--font-size-lg);
}
The base .btn class defines the structure once. Variant classes only override the specific custom properties they need to change. This is dramatically cleaner than the traditional approach of repeating every property in each variant class, and it composes naturally: class="btn btn-danger btn-lg" works because each modifier class only touches its relevant properties.
Animation with Custom Properties
Custom properties can be animated using @property (registered custom properties), enabling transitions and animations that were previously impossible:
/* Register the property so the browser knows it is a color */
@property --gradient-start {
syntax: '<color>';
initial-value: #3b82f6;
inherits: false;
}
@property --gradient-end {
syntax: '<color>';
initial-value: #8b5cf6;
inherits: false;
}
.animated-gradient {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
transition: --gradient-start 600ms ease, --gradient-end 600ms ease;
}
.animated-gradient:hover {
--gradient-start: #ef4444;
--gradient-end: #f59e0b;
}
/* The gradient smoothly transitions between color sets on hover */
Without @property registration, the browser cannot interpolate custom properties because it does not know their type. Registering with a specific syntax (<color>, <length>, <number>, etc.) unlocks smooth transitions and keyframe animations.
/* Animated counter with @property */
@property --count {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.counter {
--count: 0;
counter-reset: num var(--count);
transition: --count 2s ease-in-out;
}
.counter::after {
content: counter(num);
}
.counter.active {
--count: 100;
}
/* Displays a number that smoothly counts from 0 to 100 */
Design Token System
For large applications, custom properties can form a layered design token system with primitive, semantic, and component-level tokens:
:root {
/* Primitive tokens — raw values, no semantic meaning */
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-900: #1e3a5f;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-800: #1f2937;
--gray-900: #111827;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
/* Semantic tokens — purpose-driven, reference primitives */
--color-bg: var(--gray-50);
--color-surface: var(--gray-100);
--color-text: var(--gray-900);
--color-primary: var(--blue-500);
--color-primary-hover: var(--blue-600);
--border-radius: var(--radius-md);
/* Component tokens — scoped to specific components */
--card-bg: var(--color-surface);
--card-radius: var(--border-radius);
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--btn-radius: var(--border-radius);
--input-border: var(--gray-300, #d1d5db);
}
/* Dark theme overrides only semantic tokens */
[data-theme="dark"] {
--color-bg: var(--gray-900);
--color-surface: var(--gray-800);
--color-text: var(--gray-50);
--color-primary: var(--blue-100);
--color-primary-hover: var(--blue-50);
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
This three-tier approach (primitive, semantic, component) makes theming maintainable at scale. Changing a theme means swapping semantic tokens. Adding a new component means referencing semantic tokens. Adjusting a single component means overriding its component-level tokens. Each layer has a clear responsibility.
Performance Considerations
CSS custom properties are well-optimized in all modern browsers. However, there are patterns that can impact performance and patterns that improve it.
What Is Fast
- Declaring dozens of custom properties on :root — the overhead is negligible. A typical design token system with 50-100 properties has zero measurable performance impact.
- Inheriting custom properties — inheritance is how CSS works fundamentally. Custom properties add no extra cost to the normal inheritance mechanism.
- Using var() in property values — the browser resolves these during style computation, which is already an optimized path. Hundreds of var() calls across a page are fine.
- Changing custom properties via class toggling — this triggers the same recalculation as any other style change. Theme switches via class changes are fast.
What to Watch
- Frequent setProperty() calls in animation loops — setting custom properties via JavaScript in a requestAnimationFrame loop triggers style recalculation on every frame. For animations, prefer CSS transitions/animations with @property registration over JavaScript-driven property updates.
- Deeply nested fallback chains —
var(--a, var(--b, var(--c, var(--d, red))))with many levels of nesting adds resolution overhead. In practice, two or three levels of nesting are fine; ten or more can be measurable. - Thousands of custom properties on a single element — this is an unusual pattern, but if you generate custom properties programmatically (e.g., from a large JSON config), keep the count reasonable.
- Changing custom properties high in the DOM tree — changing a property on
:rootthat is used by many elements forces a global style recalculation. For frequently changing values (like mouse position), set the property on the most specific element possible rather than on:root.
Performance Best Practices
/* GOOD: Set mouse-tracking variables on the element, not :root */
.interactive-card {
--x: 50%;
--y: 50%;
background: radial-gradient(at var(--x) var(--y), ...);
}
/* LESS IDEAL: Setting on :root forces global recalculation */
:root {
--mouse-x: 50%; /* Every element re-checks this */
--mouse-y: 50%;
}
/* GOOD: Use @property for animated values — hardware accelerated */
@property --rotation {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.spinner {
transform: rotate(var(--rotation));
animation: spin 1s linear infinite;
}
@keyframes spin {
to { --rotation: 360deg; }
}
Browser Support
CSS Custom Properties have excellent browser support:
- Chrome — since version 49 (March 2016)
- Firefox — since version 31 (July 2014)
- Safari — since version 9.1 (March 2016)
- Edge — since version 15 (April 2017)
- Opera — since version 36 (March 2016)
As of 2026, CSS custom properties have over 98% global browser support. They are safe to use without any fallbacks or polyfills in all current projects. The only browsers that lack support are Internet Explorer (all versions) and very old mobile browsers that are no longer in active use.
The @property rule for registering custom properties has slightly newer support:
- Chrome — since version 85 (August 2020)
- Firefox — since version 128 (July 2024)
- Safari — since version 15.4 (March 2022)
- Edge — since version 85 (August 2020)
The @property rule is supported in all current browser versions as of 2026. You can use it for animated custom properties, typed custom properties, and initial value declarations with confidence.
Common Patterns Quick Reference
Here is a concise reference of the most useful CSS custom property patterns:
/* 1. Global design tokens */
:root {
--color-primary: #3b82f6;
--spacing-md: 1rem;
--radius: 8px;
}
/* 2. Component with customizable properties */
.card {
--card-padding: var(--spacing-md);
padding: var(--card-padding);
border-radius: var(--radius);
}
/* 3. Dark mode toggle */
:root { --bg: white; --text: black; }
.dark { --bg: #0f1117; --text: #e4e4e7; }
/* 4. Responsive values */
:root { --cols: 1; }
@media (min-width: 768px) { :root { --cols: 2; } }
@media (min-width: 1024px) { :root { --cols: 3; } }
/* 5. Computed values with calc() */
.spacer { height: calc(var(--spacing-md) * 2); }
/* 6. Fallback chain */
.element { color: var(--override, var(--theme-color, #3b82f6)); }
/* 7. JavaScript bridge */
/* JS: el.style.setProperty('--progress', '75%'); */
.bar { width: var(--progress, 0%); }
/* 8. Animated with @property */
@property --hue { syntax: '<number>'; initial-value: 0; inherits: false; }
.rainbow { color: hsl(var(--hue), 80%, 60%); animation: hue 3s linear infinite; }
@keyframes hue { to { --hue: 360; } }
/* 9. Scoped component theming */
.sidebar { --accent: purple; }
.footer { --accent: green; }
.link { color: var(--accent, blue); }
/* 10. Conditional logic via space toggle (advanced) */
.toggle {
--is-on: ; /* Space = truthy */
color: var(--is-on, red) green;
}
Frequently Asked Questions
What is the difference between CSS custom properties and preprocessor variables (Sass, Less)?
CSS custom properties are native to the browser and exist at runtime, meaning they can be changed dynamically with JavaScript, respond to media queries, and cascade through the DOM like any other CSS property. Preprocessor variables (Sass $variables, Less @variables) are compiled away at build time and produce static CSS output. CSS custom properties support inheritance, scoping, fallback values, and can be updated without recompiling. Preprocessor variables support operations like loops and conditionals at build time. In modern development, CSS custom properties are preferred for theming and runtime values, while preprocessor variables are used for build-time constants and code generation.
Can I use CSS variables for media queries or property names?
No, CSS custom properties cannot be used in media query conditions or as property names. You cannot write @media (min-width: var(--breakpoint)) or var(--prop): value. Custom properties can only be used as property values via the var() function. However, you can change the value of a custom property inside a media query block, which achieves a similar effect. For example, define --columns: 1 by default and set --columns: 3 inside a @media (min-width: 768px) block, then use grid-template-columns: repeat(var(--columns), 1fr). This pattern gives you responsive behavior driven by custom properties.
Do CSS custom properties affect page performance?
CSS custom properties have minimal performance impact for typical usage. The browser resolves var() references during style computation, which adds negligible overhead for dozens or even hundreds of properties. Performance concerns only arise with deeply nested fallback chains, extremely frequent JavaScript updates via setProperty() that trigger layout recalculations, or thousands of custom properties on a single element. For standard theming, component styling, and design token systems, CSS custom properties are fast and well-optimized in all modern browsers.
Summary
CSS custom properties have become an essential part of modern CSS development. They provide a native, runtime-capable variable system that integrates with the cascade, inheritance, JavaScript, and the entire CSS ecosystem. Here is what to take away from this guide:
- Declare with
--name, use withvar(--name). Define on:rootfor global availability, or on specific elements for local scope. - Custom properties cascade and inherit through the DOM tree. Override them on any element to create scoped themes and component variants.
- Fallback values (
var(--name, fallback)) provide defaults and enable layered configuration with nestedvar()chains. - Dynamic theming is the primary use case. Define theme-dependent values as custom properties and swap them via class toggles,
data-attributes, orprefers-color-schememedia queries. - JavaScript integration via
getComputedStyleandsetPropertybridges CSS and JS without inline styles. @propertyregistration unlocks animated custom properties and type-safe values.- Design token systems with primitive, semantic, and component-level layers keep large codebases maintainable.
- Performance is excellent for all typical usage patterns. Scope property changes to the most specific element when using JavaScript-driven updates.
Custom properties replace the theming aspects of preprocessor variables, eliminate JavaScript-driven inline styles for dynamic values, and enable component architectures that were previously impossible in plain CSS. They are supported in every current browser, require no build tools, and compose naturally with the rest of CSS. If you are building any front-end project in 2026, CSS custom properties should be foundational to your styling approach.
For more CSS techniques and tools, explore our CSS Grid Complete Guide for layout, the CSS Animations Guide for motion, and the CSS Gradients Guide for visual effects. Use our CSS Beautifier to keep your stylesheets clean and our CSS Minifier to optimize for production.