CSS Custom Properties: The Complete Guide for 2026

February 12, 2026

CSS Custom Properties — often called CSS Variables — have fundamentally changed how we write and maintain stylesheets. They give you a native, runtime variable system that participates in the cascade, inherits through the DOM, responds to media queries and state changes, and can be manipulated with JavaScript. No build step required. No preprocessor needed. Just CSS, doing what CSS was always supposed to do: adapt to context.

This guide covers everything you need to build production-ready systems with CSS custom properties: the declaration and usage syntax, scope and inheritance mechanics, fallback values, design token architectures, theming and dark mode systems, responsive design patterns, animation with @property, JavaScript integration, performance considerations, and how custom properties compare to preprocessor variables. Every section includes code you can copy and adapt immediately.

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

What Are CSS Custom Properties and Why They Matter

A CSS custom property is a property defined by the author (you) that stores a value and can be referenced throughout a stylesheet. Custom properties are declared with a name starting with two dashes (--) and consumed using the var() function.

:root {
    --brand-color: #3b82f6;
}

.button {
    background: var(--brand-color);
}

That looks simple, and it is. But the implications are significant. Before custom properties existed, changing a color used in 50 places meant a find-and-replace operation (or a preprocessor variable that compiled to static output). With custom properties, you change one value at runtime and every element referencing it updates automatically. No recompilation. No JavaScript. No React re-render.

Custom properties matter because they solve three problems that preprocessor variables cannot:

Syntax: Declaring with -- and Using with var()

Declaration

Custom properties are declared inside any CSS rule block. The name must begin with -- followed by one or more characters. Names are case-sensitive.

/* Declaring custom properties */
:root {
    --color-primary: #3b82f6;
    --font-size-base: 1rem;
    --spacing-unit: 0.5rem;
    --border-radius: 8px;
    --shadow-card: 0 4px 6px rgba(0, 0, 0, 0.1);
    --transition-default: 200ms ease-in-out;
}

/* Names are case-sensitive: these are two different properties */
--myColor: red;
--mycolor: blue;

The value of a custom property can be virtually anything valid in CSS: colors, lengths, strings, numbers, gradients, entire shorthand values, even multiple values separated by commas.

:root {
    /* Single values */
    --gap: 1.5rem;
    --font: 'Inter', sans-serif;

    /* Complex values */
    --gradient: linear-gradient(135deg, #667eea, #764ba2);
    --grid-cols: repeat(3, 1fr);
    --animation: slide-in 300ms ease-out forwards;

    /* Partial values (used inside calc or combined properties) */
    --hue: 220;
    --alpha: 0.8;
}

Usage with var()

The var() function retrieves the current computed value of a custom property. It can be used anywhere a CSS value is expected.

.card {
    background: var(--color-surface);
    border-radius: var(--border-radius);
    padding: var(--spacing-unit);
    box-shadow: var(--shadow-card);
    font-family: var(--font);
    transition: transform var(--transition-default);
}

/* Inside calc() */
.sidebar {
    width: calc(var(--spacing-unit) * 40);
}

/* Building composite values */
.highlight {
    color: hsl(var(--hue), 80%, 60%);
    background: hsla(var(--hue), 80%, 60%, var(--alpha));
}

One critical limitation: var() cannot be used in property names, selectors, or media query conditions. It can only appear where a property value is expected.

/* INVALID: var() in property name */
var(--prop-name): 10px;

/* INVALID: var() in media query condition */
@media (min-width: var(--breakpoint)) { }

/* VALID: var() in the value inside a media query block */
@media (min-width: 768px) {
    :root { --columns: 3; }
}

Scope and Inheritance: Global vs Local

Custom properties follow the same cascading and inheritance rules as any other CSS property. This is the key feature that separates them from preprocessor variables.

Global Scope with :root

Properties declared on :root (the <html> element) are available to every element in the document because all elements inherit from the root.

:root {
    --color-text: #e4e4e7;
    --color-bg: #0f0f23;
}

/* Both of these inherit --color-text from :root */
.header { color: var(--color-text); }
.footer { color: var(--color-text); }

Local Scope with Element Selectors

When you declare a custom property on a specific element, it overrides the inherited value for that element and all its descendants.

:root {
    --accent: #3b82f6;  /* blue globally */
}

.sidebar {
    --accent: #10b981;  /* green in sidebar */
}

.danger-zone {
    --accent: #ef4444;  /* red in danger zone */
}

/* This button takes whatever --accent is in its context */
.btn-accent {
    background: var(--accent);
    color: white;
}

In this example, a .btn-accent inside .sidebar is green, one inside .danger-zone is red, and one anywhere else is blue. The button component does not need to know about its context. It just reads --accent from whatever ancestor defines it. This is the foundation of contextual theming.

Inheritance Through the DOM Tree

Custom properties inherit exactly like color or font-family. If an element does not define a custom property, it inherits the value from its parent, which inherits from its parent, all the way up to :root.

<div class="theme-dark">          <!-- --text: #fff -->
    <div class="card">             <!-- inherits --text: #fff -->
        <p>Dark card text</p>     <!-- inherits --text: #fff -->
    </div>
    <div class="card theme-light"> <!-- --text: #111 (overridden) -->
        <p>Light card text</p>    <!-- inherits --text: #111 -->
    </div>
</div>
.theme-dark  { --text: #ffffff; --bg: #1a1a2e; }
.theme-light { --text: #111827; --bg: #ffffff; }

.card {
    color: var(--text);
    background: var(--bg);
}

Fallback Values

The var() function accepts an optional second argument: a fallback value used when the referenced property is not defined.

.element {
    /* If --accent is not defined, use #3b82f6 */
    color: var(--accent, #3b82f6);

    /* If --gap is not defined, use 1rem */
    padding: var(--gap, 1rem);

    /* Fallback can be another var() — nested fallbacks */
    background: var(--bg-override, var(--bg-default, #1a1a2e));
}

Fallback values are resolved at computed-value time. If the referenced property exists but resolves to an invalid value for the property being set, the fallback is not used. Instead, the declaration becomes "invalid at computed value time" (IACVT), and the property resets to its inherited or initial value. This is a common source of confusion.

:root {
    --width: blue;  /* a color, not a length */
}

.box {
    /* --width exists but "blue" is invalid for width.
       The fallback 100px is NOT used.
       width becomes "unset" (auto for block elements). */
    width: var(--width, 100px);
}

The rule is: the fallback only applies when the property is not defined at all (or is a guaranteed-invalid value from a failed @property registration). If the property is defined but holds a value incompatible with the context, you get IACVT behavior, not the fallback.

Dynamic Updates with JavaScript

One of the most powerful aspects of CSS custom properties is that JavaScript can read and write them at runtime, and the UI updates immediately without a page reload or framework re-render.

Reading Custom Properties

// Read a custom property from an element
const root = document.documentElement;
const primaryColor = getComputedStyle(root)
    .getPropertyValue('--color-primary')
    .trim();

console.log(primaryColor); // "#3b82f6"

Writing Custom Properties

// Set a custom property on :root (affects entire document)
document.documentElement.style.setProperty('--color-primary', '#ef4444');

// Set on a specific element (scoped override)
const sidebar = document.querySelector('.sidebar');
sidebar.style.setProperty('--accent', '#10b981');

// Remove a custom property override
sidebar.style.removeProperty('--accent');

Practical Example: User-Controlled Theme

// Range slider controlling font size
const slider = document.querySelector('#font-size-slider');
slider.addEventListener('input', (e) => {
    document.documentElement.style.setProperty(
        '--font-size-base',
        e.target.value + 'px'
    );
});

// Color picker controlling accent color
const picker = document.querySelector('#accent-picker');
picker.addEventListener('input', (e) => {
    document.documentElement.style.setProperty(
        '--color-primary',
        e.target.value
    );
});

Tracking Mouse Position

document.addEventListener('mousemove', (e) => {
    document.documentElement.style.setProperty('--mouse-x', e.clientX + 'px');
    document.documentElement.style.setProperty('--mouse-y', e.clientY + 'px');
});
.spotlight {
    background: radial-gradient(
        circle at var(--mouse-x) var(--mouse-y),
        rgba(59, 130, 246, 0.15) 0%,
        transparent 50%
    );
}

This pattern is extremely efficient because you are only setting two properties on the root element. The browser handles all the rendering updates through the normal style computation pipeline — no DOM manipulation, no layout thrashing.

Design Tokens and Theming Systems

Design tokens are the atomic building blocks of a design system: colors, spacing, typography scales, shadows, and radii. CSS custom properties are the natural implementation mechanism for design tokens in the browser.

Three-Layer Token Architecture

Production design systems typically use three layers of custom properties:

/* Layer 1: Primitive tokens (raw values) */
:root {
    --blue-50:  #eff6ff;
    --blue-100: #dbeafe;
    --blue-500: #3b82f6;
    --blue-600: #2563eb;
    --blue-900: #1e3a8a;

    --gray-50:  #f9fafb;
    --gray-100: #f3f4f6;
    --gray-700: #374151;
    --gray-800: #1f2937;
    --gray-900: #111827;

    --spacing-1: 0.25rem;
    --spacing-2: 0.5rem;
    --spacing-3: 0.75rem;
    --spacing-4: 1rem;
    --spacing-6: 1.5rem;
    --spacing-8: 2rem;

    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-full: 9999px;
}

/* Layer 2: Semantic tokens (purpose-driven) */
:root {
    --color-primary:    var(--blue-500);
    --color-primary-hover: var(--blue-600);
    --color-bg:         var(--gray-900);
    --color-surface:    var(--gray-800);
    --color-text:       var(--gray-50);
    --color-text-muted: var(--gray-700);
    --color-border:     var(--gray-700);

    --space-inline:  var(--spacing-4);
    --space-stack:   var(--spacing-6);
    --space-section: var(--spacing-8);

    --radius-card:   var(--radius-md);
    --radius-button: var(--radius-md);
    --radius-input:  var(--radius-sm);
}

/* Layer 3: Component tokens (scoped to UI elements) */
.btn {
    --btn-bg:      var(--color-primary);
    --btn-bg-hover: var(--color-primary-hover);
    --btn-text:    #ffffff;
    --btn-padding: var(--spacing-2) var(--spacing-4);
    --btn-radius:  var(--radius-button);

    background: var(--btn-bg);
    color: var(--btn-text);
    padding: var(--btn-padding);
    border-radius: var(--btn-radius);
}

.btn:hover {
    background: var(--btn-bg-hover);
}

This layered approach means you can swap the entire visual theme by overriding only the semantic layer. The primitive values stay the same (blue is still blue), but what "primary" means changes depending on context.

Dark Mode Implementation with Custom Properties

Dark mode is the most common theming use case for CSS custom properties. There are three implementation strategies, and the best approach combines two of them.

Strategy 1: prefers-color-scheme Media Query

/* Light theme (default) */
:root {
    --bg-primary:   #ffffff;
    --bg-secondary: #f3f4f6;
    --text-primary: #111827;
    --text-muted:   #6b7280;
    --border-color: #e5e7eb;
    --shadow:       0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Dark theme (automatic via OS preference) */
@media (prefers-color-scheme: dark) {
    :root {
        --bg-primary:   #0f0f23;
        --bg-secondary: #1a1a2e;
        --text-primary: #e4e4e7;
        --text-muted:   #9ca3af;
        --border-color: rgba(255, 255, 255, 0.08);
        --shadow:       0 1px 3px rgba(0, 0, 0, 0.4);
    }
}

Strategy 2: Class or Data Attribute Toggle

/* Light theme */
:root,
[data-theme="light"] {
    --bg-primary:   #ffffff;
    --bg-secondary: #f3f4f6;
    --text-primary: #111827;
    --text-muted:   #6b7280;
}

/* Dark theme */
[data-theme="dark"] {
    --bg-primary:   #0f0f23;
    --bg-secondary: #1a1a2e;
    --text-primary: #e4e4e7;
    --text-muted:   #9ca3af;
}
// Toggle dark mode
const toggle = document.querySelector('#theme-toggle');
toggle.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';
    document.documentElement.setAttribute('data-theme', next);
    localStorage.setItem('theme', next);
});

// Restore preference on load
const saved = localStorage.getItem('theme');
if (saved) {
    document.documentElement.setAttribute('data-theme', saved);
}

Strategy 3: Combined (Recommended)

The best approach respects the OS preference by default but lets users override it manually.

/* Default: respect OS preference */
:root {
    --bg-primary:   #ffffff;
    --text-primary: #111827;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg-primary:   #0f0f23;
        --text-primary: #e4e4e7;
    }
}

/* Manual override: explicit dark */
[data-theme="dark"] {
    --bg-primary:   #0f0f23;
    --text-primary: #e4e4e7;
}

/* Manual override: explicit light */
[data-theme="light"] {
    --bg-primary:   #ffffff;
    --text-primary: #111827;
}

With this setup, users who never touch the toggle get automatic dark/light based on their OS. Users who click the toggle get their explicit choice, which overrides the OS preference.

Responsive Design with Custom Properties

Custom properties excel at responsive design because you can redefine values at different breakpoints while keeping all usage sites unchanged.

Responsive Spacing and Typography

:root {
    --font-size-h1: 1.75rem;
    --font-size-h2: 1.35rem;
    --font-size-body: 0.95rem;
    --section-padding: 1.5rem;
    --grid-columns: 1;
}

@media (min-width: 768px) {
    :root {
        --font-size-h1: 2.25rem;
        --font-size-h2: 1.65rem;
        --font-size-body: 1rem;
        --section-padding: 3rem;
        --grid-columns: 2;
    }
}

@media (min-width: 1200px) {
    :root {
        --font-size-h1: 3rem;
        --font-size-h2: 2rem;
        --font-size-body: 1.05rem;
        --section-padding: 4rem;
        --grid-columns: 3;
    }
}

/* Usage is breakpoint-agnostic */
h1 { font-size: var(--font-size-h1); }
h2 { font-size: var(--font-size-h2); }
p  { font-size: var(--font-size-body); }

.content-grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-columns), 1fr);
    gap: var(--section-padding);
    padding: var(--section-padding);
}

This pattern centralizes all responsive changes into the :root media query blocks. Your component CSS never needs its own media queries — it just reads the custom properties and adapts.

Container Queries with Custom Properties

.widget-container {
    container-type: inline-size;
}

.widget {
    --widget-direction: column;
    --widget-gap: 0.75rem;
    --widget-img-width: 100%;
}

@container (min-width: 400px) {
    .widget {
        --widget-direction: row;
        --widget-gap: 1.5rem;
        --widget-img-width: 40%;
    }
}

.widget {
    display: flex;
    flex-direction: var(--widget-direction);
    gap: var(--widget-gap);
}

.widget img {
    width: var(--widget-img-width);
}

Animation with Custom Properties

By default, CSS cannot transition or animate custom properties because the browser treats their values as opaque strings. The @property at-rule changes this by registering a custom property with a specific type.

The @property At-Rule

@property --hue {
    syntax: '<number>';
    inherits: false;
    initial-value: 220;
}

@property --progress {
    syntax: '<percentage>';
    inherits: false;
    initial-value: 0%;
}

@property --gradient-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
}

The syntax descriptor tells the browser what type of value the property holds. Supported types include <number>, <integer>, <length>, <percentage>, <color>, <angle>, <length-percentage>, <time>, and more. Once the browser knows the type, it can interpolate between values during transitions and animations.

Animated Gradient Background

@property --gradient-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
}

.animated-gradient {
    background: linear-gradient(
        var(--gradient-angle),
        #667eea,
        #764ba2
    );
    animation: rotate-gradient 3s linear infinite;
}

@keyframes rotate-gradient {
    to { --gradient-angle: 360deg; }
}

Without @property, this animation would not work. The browser would see --gradient-angle as a string and snap between "0deg" and "360deg" instead of smoothly interpolating.

Animated Color Shift

@property --hue {
    syntax: '<number>';
    inherits: false;
    initial-value: 220;
}

.color-shift {
    background: hsl(var(--hue), 70%, 50%);
    transition: --hue 600ms ease;
}

.color-shift:hover {
    --hue: 340;
}

Progress Bar Animation

@property --progress {
    syntax: '<percentage>';
    inherits: false;
    initial-value: 0%;
}

.progress-bar {
    background: linear-gradient(
        90deg,
        #3b82f6 var(--progress),
        #1f2937 var(--progress)
    );
    transition: --progress 1s ease-out;
    height: 8px;
    border-radius: 4px;
}

/* Set via JavaScript or class toggle */
.progress-bar.loaded { --progress: 100%; }

@property Rule for Typed Custom Properties

Beyond enabling animation, @property provides type safety, initial values, and controlled inheritance for custom properties.

Type Safety

@property --spacing {
    syntax: '<length>';
    inherits: true;
    initial-value: 1rem;
}

/* If someone sets --spacing to an invalid value like "blue",
   the browser uses the initial-value (1rem) instead of
   triggering IACVT behavior */
.box {
    padding: var(--spacing);
}

Controlling Inheritance

@property --card-bg {
    syntax: '<color>';
    inherits: false;       /* Does NOT inherit from parent */
    initial-value: #1a1a2e;
}

/* Each .card gets #1a1a2e unless it explicitly sets --card-bg.
   Nested cards do not inherit their parent card's --card-bg. */
.card {
    background: var(--card-bg);
}

Supported @property Syntax Types

The syntax descriptor supports these types: <number>, <integer>, <length>, <percentage>, <length-percentage>, <color>, <angle>, <time>, <resolution>, <image>, and <transform-function>. You can combine types with | for alternatives, use + for space-separated lists, and # for comma-separated lists:

@property --size   { syntax: '<length> | auto'; inherits: false; initial-value: auto; }
@property --sizes  { syntax: '<length>+';       inherits: false; initial-value: 0px; }
@property --colors { syntax: '<color>#';        inherits: false; initial-value: black; }

Real-World Patterns and Best Practices

Component API Pattern

Expose custom properties as the public API of a component. Consumers customize behavior by overriding properties, not by writing new CSS rules.

/* Card component with configurable properties */
.card {
    --card-padding: 1.5rem;
    --card-radius: 8px;
    --card-bg: #1a1a2e;
    --card-border: rgba(255, 255, 255, 0.08);
    --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);

    padding: var(--card-padding);
    border-radius: var(--card-radius);
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    box-shadow: var(--card-shadow);
}

/* Usage: customize without touching .card internals */
.featured-card {
    --card-bg: #1e293b;
    --card-border: rgba(59, 130, 246, 0.3);
    --card-shadow: 0 8px 24px rgba(59, 130, 246, 0.15);
}

.compact-card {
    --card-padding: 0.75rem;
    --card-radius: 4px;
}

Spacing Scale System

:root {
    --space-unit: 0.25rem;  /* 4px base */
    --space-1:  calc(var(--space-unit) * 1);   /* 4px */
    --space-2:  calc(var(--space-unit) * 2);   /* 8px */
    --space-3:  calc(var(--space-unit) * 3);   /* 12px */
    --space-4:  calc(var(--space-unit) * 4);   /* 16px */
    --space-6:  calc(var(--space-unit) * 6);   /* 24px */
    --space-8:  calc(var(--space-unit) * 8);   /* 32px */
    --space-12: calc(var(--space-unit) * 12);  /* 48px */
    --space-16: calc(var(--space-unit) * 16);  /* 64px */
}

/* Change the entire scale by adjusting one value */
@media (min-width: 1400px) {
    :root { --space-unit: 0.3rem; }  /* Slightly larger on wide screens */
}

Fluid Typography with Custom Properties and clamp()

:root {
    --fluid-min-width: 320;
    --fluid-max-width: 1200;

    --font-size-sm:   clamp(0.8rem, 0.75rem + 0.25vw, 0.9rem);
    --font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
    --font-size-lg:   clamp(1.25rem, 1rem + 1vw, 1.5rem);
    --font-size-xl:   clamp(1.5rem, 1rem + 2vw, 2.25rem);
    --font-size-2xl:  clamp(2rem, 1.25rem + 3vw, 3.5rem);
}

Consistent Border and Focus Styles

:root {
    --focus-ring-color: rgba(59, 130, 246, 0.5);
    --focus-ring-offset: 2px;
    --focus-ring-width: 3px;
}

/* Reusable focus style */
:focus-visible {
    outline: var(--focus-ring-width) solid var(--focus-ring-color);
    outline-offset: var(--focus-ring-offset);
}

/* High contrast mode override */
@media (forced-colors: active) {
    :root {
        --focus-ring-color: Highlight;
    }
}

Color Palette from a Single Hue

:root {
    --hue: 220;

    --color-50:  hsl(var(--hue), 90%, 96%);
    --color-100: hsl(var(--hue), 85%, 90%);
    --color-200: hsl(var(--hue), 80%, 80%);
    --color-300: hsl(var(--hue), 75%, 70%);
    --color-400: hsl(var(--hue), 70%, 60%);
    --color-500: hsl(var(--hue), 65%, 50%);
    --color-600: hsl(var(--hue), 70%, 40%);
    --color-700: hsl(var(--hue), 75%, 30%);
    --color-800: hsl(var(--hue), 80%, 20%);
    --color-900: hsl(var(--hue), 85%, 10%);
}

/* Change the entire palette by changing one number */
.theme-purple { --hue: 270; }
.theme-teal   { --hue: 175; }
.theme-rose   { --hue: 350; }

Naming Conventions

Use lowercase with hyphens (--color-primary), prefix by category (--color-*, --space-*, --font-*), prefer semantic names in the semantic layer (--color-text-muted over --gray-400), namespace component properties (--card-bg, --btn-radius), and include state in the name where needed (--btn-bg-hover, --input-border-focus).

Performance Considerations

CSS custom properties are well-optimized in all modern browsers, but understanding the performance characteristics helps you avoid edge cases.

What Is Fast

What to Watch Out For

Performance Best Practice

/* Scope dynamic properties to the element that needs them */

/* LESS OPTIMAL: setting on root, recalculates entire document */
document.documentElement.style.setProperty('--tooltip-x', x + 'px');

/* MORE OPTIMAL: setting on the specific element */
tooltip.style.setProperty('--tooltip-x', x + 'px');
/* Throttle high-frequency updates */
let ticking = false;
window.addEventListener('scroll', () => {
    if (!ticking) {
        requestAnimationFrame(() => {
            document.documentElement.style.setProperty(
                '--scroll-y',
                window.scrollY + 'px'
            );
            ticking = false;
        });
        ticking = true;
    }
});

Browser Support

CSS custom properties (var()) have been supported in all major browsers since 2017:

Global browser support in 2026 exceeds 97%. There is no practical reason to avoid custom properties in any modern web project.

The @property at-rule has slightly newer support:

As of 2026, @property is supported in all current browser versions. For projects that must support older Firefox versions (pre-128), use @property as progressive enhancement: the animation will snap instead of interpolate, but the layout and styling remain correct.

Comparison with Preprocessor Variables (Sass/Less)

If you are migrating from Sass or Less, understanding the differences helps you choose the right tool for each job.

/* Sass variable: compiled away, static output */
$primary: #3b82f6;
.btn { background: $primary; }
/* Compiled CSS: .btn { background: #3b82f6; } */

/* CSS custom property: exists at runtime, dynamic */
:root { --primary: #3b82f6; }
.btn { background: var(--primary); }
/* Output CSS is the same as the source */
Feature CSS Custom Properties Sass/Less Variables
Runtime? Yes — live in the browser No — compiled to static CSS
Cascade and inherit? Yes — follows DOM tree No — lexical scope only
JavaScript access? Yes — read/write via CSSOM No — not accessible after compilation
Media query responsive? Yes — redefine values per breakpoint No — values are fixed at compile time
Loops and conditionals? No Yes — @each, @for, @if
Mixins and functions? No Yes — @mixin, @function
Build tools required? No Yes — Sass/Less compiler
Theming support? Excellent — runtime theme switching Limited — requires generating multiple stylesheets

The modern approach is to use both: Sass for build-time code generation (loops, math, utility class generation) and CSS custom properties for anything dynamic at runtime (theming, user preferences, responsive values). Sass can generate your custom property declarations at build time, producing clean CSS that the browser uses dynamically.

Frequently Asked Questions

What is the difference between CSS custom properties and Sass variables?

CSS custom properties exist at runtime in the browser: they cascade, inherit through the DOM, and can be changed dynamically with JavaScript or media queries. Sass variables compile to static values at build time. Use custom properties for theming and runtime values; use preprocessor variables for build-time code generation.

Can CSS custom properties be animated?

Not by default — the browser treats them as opaque strings. Register a property with @property to declare its type (<color>, <length>, <number>), and the browser can then interpolate between values for smooth transitions and keyframe animations.

How do I implement dark mode with custom properties?

Define your color palette as custom properties on :root, then override them inside @media (prefers-color-scheme: dark) or a [data-theme="dark"] selector. For manual toggle support, use JavaScript to set a data-theme attribute on <html> and persist the choice in localStorage.

What are design tokens?

Design tokens are the atomic values of a design system. CSS custom properties implement them in layers: primitives (--blue-500), semantic tokens (--color-primary: var(--blue-500)), and component tokens (--btn-bg: var(--color-primary)). Changing one primitive cascades to every referencing component.

Do custom properties affect performance?

For typical usage, no. Browsers handle hundreds of custom properties with negligible overhead. Avoid deeply nested fallback chains (3+ levels), and scope high-frequency JavaScript setProperty() calls to the most specific element rather than :root.

What happens when a custom property is undefined?

The var() function uses its fallback if provided (var(--missing, blue) resolves to blue). Without a fallback, the property gets the CSS unset value. If the property exists but holds an incompatible value, the fallback is not used — the declaration becomes "invalid at computed value time" (IACVT) and resets to the inherited or initial value.

Summary

CSS custom properties are the foundation of modern CSS architecture. They provide a native variable system that works at runtime, respects the cascade, inherits through the DOM, and integrates seamlessly with JavaScript. 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 scoped overrides.
  2. Custom properties cascade and inherit through the DOM tree, enabling contextual theming where components adapt to their location without explicit configuration.
  3. Fallback values (var(--name, fallback)) provide defaults, but understand the IACVT behavior when a property exists with an incompatible value.
  4. Design token systems with primitive, semantic, and component layers keep large codebases maintainable and themeable.
  5. Dark mode is best implemented by combining prefers-color-scheme for automatic detection with a data-theme attribute for manual override.
  6. Responsive design benefits from redefining custom property values in media query blocks, centralizing breakpoint logic away from component CSS.
  7. @property unlocks animated custom properties, type safety, and controlled inheritance — register typed properties whenever you need transitions or animation.
  8. JavaScript integration via getComputedStyle and setProperty bridges CSS and JS efficiently. Scope dynamic updates to the most specific element to minimize recalculation.
  9. Combine with preprocessors: use Sass/Less for build-time generation and CSS custom properties for runtime dynamism.

Custom properties are supported in every current browser with over 97% global coverage. There is no reason to defer adoption. Whether you are building a component library, a design system, a theme engine, or just trying to keep your colors consistent, CSS custom properties should be foundational to your approach.

For more CSS techniques, explore our CSS Grid Complete Guide for layout, the CSS Nesting Guide for modern selector syntax, and the CSS Selectors Cheat Sheet for quick selector reference. Use our Color Picker to find the perfect values for your custom properties and the CSS Gradient Generator to create gradient values.

Related Resources

Color Picker
Pick and convert color values for your CSS custom properties
CSS Gradient Generator
Create gradient values for CSS variables
CSS Grid Complete Guide
Master two-dimensional CSS layout with Grid
CSS Variables Quick Guide
Quick reference for CSS custom properties basics
CSS Selectors Cheat Sheet
Quick reference for all CSS selector types
CSS Nesting Complete Guide
Modern native CSS nesting syntax and patterns