CSS Custom Properties (Variables): The Complete Guide for 2026

February 11, 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.

⚙ Try it: Experiment with color values for your CSS variables using our Color Palette Generator, or create gradient values with the CSS Gradient Generator.

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');
⚙ Build your palette: Generate harmonious color schemes for your CSS variables with our Color Palette Generator. Export colors as CSS custom properties ready to paste into your theme.

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.

⚙ Clean up your CSS: After building your custom property system, use our CSS Beautifier to format your stylesheets consistently, or the CSS Minifier to optimize for production.

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

What to Watch

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:

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:

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:

  1. Declare with --name, use with var(--name). Define on :root for global availability, or on specific elements for local scope.
  2. Custom properties cascade and inherit through the DOM tree. Override them on any element to create scoped themes and component variants.
  3. Fallback values (var(--name, fallback)) provide defaults and enable layered configuration with nested var() chains.
  4. Dynamic theming is the primary use case. Define theme-dependent values as custom properties and swap them via class toggles, data- attributes, or prefers-color-scheme media queries.
  5. JavaScript integration via getComputedStyle and setProperty bridges CSS and JS without inline styles.
  6. @property registration unlocks animated custom properties and type-safe values.
  7. Design token systems with primitive, semantic, and component-level layers keep large codebases maintainable.
  8. 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.

Related Resources

Color Palette Generator
Generate color schemes for CSS custom properties
CSS Gradient Generator
Create gradient values for your CSS variables
CSS Beautifier
Format and prettify CSS code with custom properties
CSS Minifier
Minify and compress CSS for production
CSS Grid Complete Guide
Master two-dimensional CSS layout
CSS Animations Guide
Keyframe animations, transitions, and performance