CSS Variables Cookbook: 20+ Practical Recipes for Custom Properties

February 12, 2026

This cookbook is a collection of practical, copy-paste-ready CSS custom property recipes for real-world projects. Each recipe solves a specific problem with working code you can drop into your stylesheets immediately. Whether you need a theme switcher, a responsive type scale, a fluid spacing system, or dynamic component variants, every pattern here has been tested and optimized for production use.

If you need a deep explanation of how CSS custom properties work under the hood, read our CSS Custom Properties Complete Guide first. This cookbook assumes you know the basics and want actionable patterns.

⚙ Try it: Generate color values for your recipes with our Color Palette Generator, or build gradients with the CSS Gradient Generator.

Quick Refresher: Declaration, Usage, Fallbacks

Every recipe in this cookbook builds on three fundamentals. Declare variables with the -- prefix, read them with var(), and provide fallbacks as the second argument to var():

/* Declare */
:root {
    --primary: #3b82f6;
    --radius: 8px;
}

/* Use */
.btn { background: var(--primary); border-radius: var(--radius); }

/* Fallback: if --accent is undefined, use #8b5cf6 */
.badge { background: var(--accent, #8b5cf6); }

/* Chained fallback */
.card { padding: var(--card-pad, var(--spacing-md, 1rem)); }

Recipe 1: Dark/Light Theme Switcher

The most common use of CSS variables. Define colors once, swap them with a class or data attribute:

/* Light theme (default) */
:root {
    --bg: #ffffff;
    --surface: #f8f9fa;
    --text: #1a1a2e;
    --text-muted: #6b7280;
    --border: #e5e7eb;
    --primary: #3b82f6;
    --shadow: 0 2px 8px rgba(0,0,0,0.08);
}

/* Dark theme */
[data-theme="dark"] {
    --bg: #0f1117;
    --surface: #1a1d27;
    --text: #e4e4e7;
    --text-muted: #9ca3af;
    --border: #2a2e3a;
    --primary: #60a5fa;
    --shadow: 0 2px 8px rgba(0,0,0,0.32);
}

/* Respect system preference when no manual choice */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #0f1117; --surface: #1a1d27;
        --text: #e4e4e7; --text-muted: #9ca3af;
        --border: #2a2e3a; --primary: #60a5fa;
        --shadow: 0 2px 8px rgba(0,0,0,0.32);
    }
}

body { background: var(--bg); color: var(--text); }
.card { background: var(--surface); border: 1px solid var(--border); }
// Toggle with one line
const btn = document.getElementById('theme-toggle');
btn.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 on load
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);

Recipe 2: HSL Color Palette from a Single Hue

Generate an entire color scale by varying saturation and lightness from one hue variable:

:root {
    --hue: 220;  /* Change this one value to recolor everything */

    --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%, 68%);
    --color-400: hsl(var(--hue), 72%, 58%);
    --color-500: hsl(var(--hue), 80%, 50%);
    --color-600: hsl(var(--hue), 85%, 42%);
    --color-700: hsl(var(--hue), 88%, 34%);
    --color-800: hsl(var(--hue), 90%, 24%);
    --color-900: hsl(var(--hue), 92%, 15%);
}

/* Swap entire palette by changing one number */
.theme-purple { --hue: 270; }
.theme-green  { --hue: 150; }
.theme-red    { --hue: 0; }

/* Usage */
.btn-primary { background: var(--color-500); color: var(--color-50); }
.btn-primary:hover { background: var(--color-600); }
.badge { background: var(--color-100); color: var(--color-800); }

Recipe 3: Responsive Typography Scale

A modular type scale driven by two variables that adapts to screen size using clamp():

:root {
    --font-base: clamp(0.938rem, 0.5vw + 0.85rem, 1.125rem);
    --scale: 1.25;  /* Major third */

    --text-xs:  calc(var(--font-base) / var(--scale));
    --text-sm:  calc(var(--font-base) / 1.125);
    --text-base: var(--font-base);
    --text-lg:  calc(var(--font-base) * var(--scale));
    --text-xl:  calc(var(--font-base) * var(--scale) * var(--scale));
    --text-2xl: calc(var(--font-base) * var(--scale) * var(--scale) * var(--scale));
    --text-3xl: calc(var(--font-base) * var(--scale) * var(--scale) * var(--scale) * var(--scale));
}

body    { font-size: var(--text-base); }
small   { font-size: var(--text-sm); }
h4      { font-size: var(--text-lg); }
h3      { font-size: var(--text-xl); }
h2      { font-size: var(--text-2xl); }
h1      { font-size: var(--text-3xl); }

The clamp() in --font-base makes the entire scale fluid: it grows smoothly from 15px on small screens to 18px on large screens, and every heading scales proportionally.

Recipe 4: Fluid Spacing System

A spacing scale that adapts to viewport size without breakpoints:

:root {
    --space-unit: clamp(0.25rem, 0.5vw, 0.375rem);

    --space-1:  calc(var(--space-unit) * 1);   /* 4-6px */
    --space-2:  calc(var(--space-unit) * 2);   /* 8-12px */
    --space-3:  calc(var(--space-unit) * 3);   /* 12-18px */
    --space-4:  calc(var(--space-unit) * 4);   /* 16-24px */
    --space-6:  calc(var(--space-unit) * 6);   /* 24-36px */
    --space-8:  calc(var(--space-unit) * 8);   /* 32-48px */
    --space-12: calc(var(--space-unit) * 12);  /* 48-72px */
    --space-16: calc(var(--space-unit) * 16);  /* 64-96px */
}

/* Usage */
.card { padding: var(--space-4); margin-bottom: var(--space-6); }
.stack > * + * { margin-top: var(--space-4); }
.section { padding-block: var(--space-12); }
.container { padding-inline: var(--space-4); }

Recipe 5: Component-Level Theming (Card)

Define a card component with its own custom property API. Consumers override the properties to customize it:

/* Card component with configurable properties */
.card {
    --card-bg: var(--surface, #1a1d27);
    --card-border: var(--border, #2a2e3a);
    --card-radius: var(--radius, 12px);
    --card-padding: var(--space-4, 1rem);
    --card-shadow: var(--shadow, 0 2px 8px rgba(0,0,0,0.12));

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

/* Variant: elevated */
.card-elevated {
    --card-shadow: 0 8px 24px rgba(0,0,0,0.2);
    --card-border: transparent;
}

/* Variant: outlined */
.card-outlined {
    --card-shadow: none;
    --card-border: var(--primary, #3b82f6);
    --card-bg: transparent;
}

/* Context override: cards in a sidebar get tighter padding */
.sidebar .card {
    --card-padding: 0.75rem;
    --card-radius: 8px;
}

Recipe 6: Button System with Variants and Sizes

.btn {
    --btn-bg: var(--primary, #3b82f6);
    --btn-text: #ffffff;
    --btn-px: 1.25rem;
    --btn-py: 0.625rem;
    --btn-radius: 8px;
    --btn-size: 0.938rem;

    display: inline-flex; align-items: center; gap: 0.5rem;
    background: var(--btn-bg); color: var(--btn-text);
    padding: var(--btn-py) var(--btn-px);
    border-radius: var(--btn-radius); font-size: var(--btn-size);
    border: 2px solid var(--btn-bg); cursor: pointer;
    transition: filter 150ms ease, transform 100ms ease;
}
.btn:hover { filter: brightness(1.12); }
.btn:active { transform: scale(0.97); }

/* Color variants — only override colors */
.btn-secondary { --btn-bg: #6b7280; }
.btn-danger    { --btn-bg: #ef4444; }
.btn-success   { --btn-bg: #10b981; }
.btn-ghost     { --btn-bg: transparent; --btn-text: var(--primary, #3b82f6); }

/* Size variants — only override dimensions */
.btn-sm { --btn-px: 0.75rem; --btn-py: 0.375rem; --btn-size: 0.813rem; }
.btn-lg { --btn-px: 1.75rem; --btn-py: 0.875rem; --btn-size: 1.063rem; }

Combine freely: class="btn btn-danger btn-lg" works because each modifier only touches its own properties.

Recipe 7: CSS Variables with calc() for Dynamic Layouts

Use variables as inputs to calculations for layouts that adapt mathematically:

:root {
    --sidebar-width: 280px;
    --header-height: 64px;
    --gap: 1.5rem;
}

.layout {
    display: grid;
    grid-template-columns: var(--sidebar-width) 1fr;
    grid-template-rows: var(--header-height) 1fr;
    gap: var(--gap);
    min-height: 100vh;
}

.main-content {
    /* Content area = full width minus sidebar and gaps */
    max-width: calc(100vw - var(--sidebar-width) - var(--gap) * 2);
    /* Viewport height minus header */
    min-height: calc(100vh - var(--header-height) - var(--gap));
}

/* Collapse sidebar on mobile */
@media (max-width: 768px) {
    :root {
        --sidebar-width: 0px;
        --header-height: 56px;
        --gap: 1rem;
    }
    .layout { grid-template-columns: 1fr; }
}

Recipe 8: Animated Gradient with @property

Register custom properties so the browser can interpolate them, enabling smooth gradient animations:

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

@property --color-a {
    syntax: '<color>';
    initial-value: #3b82f6;
    inherits: false;
}

@property --color-b {
    syntax: '<color>';
    initial-value: #8b5cf6;
    inherits: false;
}

.gradient-bg {
    background: linear-gradient(
        var(--grad-angle),
        var(--color-a),
        var(--color-b)
    );
    animation: rotate-gradient 4s linear infinite;
}

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

/* Hover to change colors smoothly */
.gradient-bg:hover {
    --color-a: #ef4444;
    --color-b: #f59e0b;
    transition: --color-a 600ms ease, --color-b 600ms ease;
}

Recipe 9: Scroll-Driven Progress Bar

JavaScript sets a CSS variable based on scroll position; CSS handles the rendering:

/* CSS */
.progress-bar {
    --scroll: 0;
    position: fixed; top: 0; left: 0;
    width: calc(var(--scroll) * 100%);
    height: 3px;
    background: var(--primary, #3b82f6);
    z-index: 1000;
    transition: width 50ms linear;
}
// JavaScript
const bar = document.querySelector('.progress-bar');
window.addEventListener('scroll', () => {
    const max = document.documentElement.scrollHeight - window.innerHeight;
    const pct = Math.min(window.scrollY / max, 1);
    bar.style.setProperty('--scroll', pct);
}, { passive: true });

Recipe 10: Mouse-Tracking Spotlight Effect

/* CSS */
.spotlight-card {
    --mx: 50%; --my: 50%;
    position: relative; overflow: hidden;
    background: var(--surface, #1a1d27);
    border-radius: 12px; padding: 2rem;
}
.spotlight-card::before {
    content: '';
    position: absolute; inset: 0;
    background: radial-gradient(
        600px circle at var(--mx) var(--my),
        rgba(59,130,246,0.1), transparent 40%
    );
    pointer-events: none;
}
// JavaScript — scoped to element for performance
document.querySelectorAll('.spotlight-card').forEach(card => {
    card.addEventListener('mousemove', (e) => {
        const r = card.getBoundingClientRect();
        card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
        card.style.setProperty('--my', (e.clientY - r.top) + 'px');
    });
});

Recipe 11: Responsive Grid with Variable Columns

:root { --grid-cols: 1; --grid-gap: 1rem; }

@media (min-width: 640px)  { :root { --grid-cols: 2; --grid-gap: 1.25rem; } }
@media (min-width: 1024px) { :root { --grid-cols: 3; --grid-gap: 1.5rem; } }
@media (min-width: 1440px) { :root { --grid-cols: 4; --grid-gap: 2rem; } }

.auto-grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols), 1fr);
    gap: var(--grid-gap);
}

Components using .auto-grid never need their own media queries. All responsive logic is centralized in the variable declarations. For more grid techniques, see our CSS Grid Complete Guide.

Recipe 12: Form Styling System

/* Form tokens */
:root {
    --input-bg: var(--surface, #1a1d27);
    --input-border: var(--border, #2a2e3a);
    --input-focus: var(--primary, #3b82f6);
    --input-radius: 8px;
    --input-px: 0.875rem;
    --input-py: 0.625rem;
    --input-font: 0.938rem;
    --label-color: var(--text-muted, #9ca3af);
    --input-error: #ef4444;
    --input-success: #10b981;
}

.form-field { margin-bottom: var(--space-4, 1rem); }

.form-label {
    display: block; margin-bottom: 0.375rem;
    font-size: 0.875rem; font-weight: 500;
    color: var(--label-color);
}

.form-input {
    width: 100%; box-sizing: border-box;
    background: var(--input-bg); color: var(--text, #e4e4e7);
    border: 1.5px solid var(--input-border);
    border-radius: var(--input-radius);
    padding: var(--input-py) var(--input-px);
    font-size: var(--input-font);
    transition: border-color 150ms ease, box-shadow 150ms ease;
}

.form-input:focus {
    outline: none;
    border-color: var(--input-focus);
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

/* Validation states override border color */
.form-input.is-error { --input-border: var(--input-error); }
.form-input.is-success { --input-border: var(--input-success); }

.form-hint {
    margin-top: 0.25rem; font-size: 0.813rem;
    color: var(--label-color);
}
.form-hint.error { color: var(--input-error); }
.form-hint.success { color: var(--input-success); }

Recipe 13: Navigation Theming

/* Nav component with its own property API */
.nav {
    --nav-bg: var(--surface, #1a1d27);
    --nav-text: var(--text-muted, #9ca3af);
    --nav-active: var(--primary, #3b82f6);
    --nav-hover: var(--text, #e4e4e7);
    --nav-height: 64px;

    display: flex; align-items: center; gap: 0.25rem;
    height: var(--nav-height);
    background: var(--nav-bg);
    padding-inline: var(--space-4, 1rem);
}

.nav-link {
    color: var(--nav-text); padding: 0.5rem 0.875rem;
    border-radius: 6px; text-decoration: none;
    font-size: 0.938rem; transition: color 150ms, background 150ms;
}
.nav-link:hover { color: var(--nav-hover); background: rgba(255,255,255,0.05); }
.nav-link.active { color: var(--nav-active); background: rgba(59,130,246,0.1); }

/* Transparent nav on hero sections */
.hero .nav { --nav-bg: transparent; --nav-text: rgba(255,255,255,0.7); --nav-hover: #fff; }

/* Compact nav on mobile */
@media (max-width: 768px) { .nav { --nav-height: 56px; } }

Recipe 14: Design Token System (Three-Tier)

A production-ready pattern with primitive, semantic, and component layers:

:root {
    /* --- Tier 1: Primitives (raw values) --- */
    --blue-50: #eff6ff; --blue-500: #3b82f6; --blue-600: #2563eb;
    --gray-50: #f9fafb; --gray-100: #f3f4f6; --gray-200: #e5e7eb;
    --gray-700: #374151; --gray-800: #1f2937; --gray-900: #111827;
    --green-500: #10b981; --red-500: #ef4444; --amber-500: #f59e0b;
    --radius-sm: 4px; --radius-md: 8px; --radius-lg: 16px; --radius-full: 9999px;

    /* --- Tier 2: Semantic (purpose-driven) --- */
    --color-bg:         var(--gray-50);
    --color-surface:    var(--gray-100);
    --color-text:       var(--gray-900);
    --color-text-muted: var(--gray-700);
    --color-border:     var(--gray-200);
    --color-primary:    var(--blue-500);
    --color-primary-fg: var(--gray-50);
    --color-success:    var(--green-500);
    --color-danger:     var(--red-500);
    --color-warning:    var(--amber-500);
    --radius:           var(--radius-md);

    /* --- Tier 3: Component tokens --- */
    --btn-radius: var(--radius);
    --card-radius: var(--radius-lg);
    --input-radius: var(--radius);
    --badge-radius: var(--radius-full);
}

/* Dark theme: only override Tier 2 */
[data-theme="dark"] {
    --color-bg:         var(--gray-900);
    --color-surface:    var(--gray-800);
    --color-text:       var(--gray-50);
    --color-text-muted: var(--gray-200);
    --color-border:     var(--gray-700);
    --color-primary:    var(--blue-50);
    --color-primary-fg: var(--gray-900);
}

This architecture scales to large teams. Designers update primitives, theme authors update semantics, and component developers reference the tokens relevant to their component.

Recipe 15: Container Query Theming

CSS variables work with container queries to create components that adapt based on their parent size:

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

.responsive-card {
    --card-layout: column;
    --card-img-width: 100%;
    --card-padding: 1rem;

    display: flex;
    flex-direction: var(--card-layout);
    padding: var(--card-padding);
}

.responsive-card img { width: var(--card-img-width); }

@container (min-width: 500px) {
    .responsive-card {
        --card-layout: row;
        --card-img-width: 40%;
        --card-padding: 1.5rem;
    }
}

@container (min-width: 800px) {
    .responsive-card {
        --card-img-width: 30%;
        --card-padding: 2rem;
    }
}

Recipe 16: Dynamic Counter Animation

@property --num {
    syntax: '<integer>';
    initial-value: 0;
    inherits: false;
}

.stat {
    --num: 0;
    --target: 100;
    counter-reset: stat var(--num);
    font-size: 2.5rem; font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.stat::after {
    content: counter(stat);
}

.stat.animate {
    animation: count-up 2s ease-out forwards;
}

@keyframes count-up {
    to { --num: var(--target); }
}
// Set different targets per element
document.querySelectorAll('.stat').forEach(el => {
    el.style.setProperty('--target', el.dataset.value);
    el.classList.add('animate');
});

Recipe 17: Aspect-Ratio Card Grid

.gallery {
    --cols: 3;
    --aspect: 4 / 3;
    display: grid;
    grid-template-columns: repeat(var(--cols), 1fr);
    gap: var(--space-4, 1rem);
}

.gallery-item {
    aspect-ratio: var(--aspect);
    overflow: hidden; border-radius: var(--radius, 12px);
    background: var(--surface, #1a1d27);
}

.gallery-item img {
    width: 100%; height: 100%; object-fit: cover;
}

/* Variant: square grid */
.gallery-square { --aspect: 1; }

/* Variant: cinematic */
.gallery-wide { --aspect: 16 / 9; --cols: 2; }

Recipe 18: Layered Shadow System

:root {
    --shadow-color: 220 15% 6%;

    --shadow-xs: 0 1px 2px hsl(var(--shadow-color) / 0.1);
    --shadow-sm: 0 1px 3px hsl(var(--shadow-color) / 0.12),
                 0 1px 2px hsl(var(--shadow-color) / 0.08);
    --shadow-md: 0 4px 6px hsl(var(--shadow-color) / 0.1),
                 0 2px 4px hsl(var(--shadow-color) / 0.06);
    --shadow-lg: 0 10px 15px hsl(var(--shadow-color) / 0.12),
                 0 4px 6px hsl(var(--shadow-color) / 0.05);
    --shadow-xl: 0 20px 25px hsl(var(--shadow-color) / 0.15),
                 0 8px 10px hsl(var(--shadow-color) / 0.06);
}

/* Light theme: lighter shadows */
[data-theme="light"] { --shadow-color: 220 10% 50%; }

.card { box-shadow: var(--shadow-sm); transition: box-shadow 200ms ease; }
.card:hover { box-shadow: var(--shadow-lg); }
.modal { box-shadow: var(--shadow-xl); }

Recipe 19: Fluid Container Widths

:root {
    --container-sm: 640px;
    --container-md: 768px;
    --container-lg: 1024px;
    --container-xl: 1280px;
    --container-px: clamp(1rem, 3vw, 2rem);
}

.container {
    width: 100%;
    max-width: var(--container-lg);
    margin-inline: auto;
    padding-inline: var(--container-px);
}

.container-sm { max-width: var(--container-sm); }
.container-md { max-width: var(--container-md); }
.container-xl { max-width: var(--container-xl); }
.container-full { max-width: none; }

Recipe 20: Interactive Theme Customizer

Let users customize the site theme in real time using JavaScript and CSS variables:

<div class="customizer">
    <label>Hue: <input type="range" id="hue" min="0" max="360" value="220"></label>
    <label>Radius: <input type="range" id="radius" min="0" max="24" value="8"></label>
    <label>Spacing: <input type="range" id="spacing" min="2" max="8" value="4"></label>
</div>
const root = document.documentElement.style;

document.getElementById('hue').addEventListener('input', e => {
    root.setProperty('--hue', e.target.value);
});
document.getElementById('radius').addEventListener('input', e => {
    root.setProperty('--radius', e.target.value + 'px');
});
document.getElementById('spacing').addEventListener('input', e => {
    root.setProperty('--space-unit', (e.target.value * 0.25) + 'rem');
});

// Persist choices
['hue', 'radius', 'spacing'].forEach(key => {
    const saved = localStorage.getItem('custom-' + key);
    if (saved) {
        document.getElementById(key).value = saved;
        document.getElementById(key).dispatchEvent(new Event('input'));
    }
    document.getElementById(key).addEventListener('change', e => {
        localStorage.setItem('custom-' + key, e.target.value);
    });
});

Recipe 21: Staggered Animation Delay

/* Set index on each child for staggered entrance */
.stagger-list > * {
    --i: 0;
    opacity: 0;
    transform: translateY(12px);
    animation: fade-in 400ms ease forwards;
    animation-delay: calc(var(--i) * 60ms);
}

@keyframes fade-in {
    to { opacity: 1; transform: translateY(0); }
}

/* Set the index inline or with nth-child */
.stagger-list > :nth-child(1)  { --i: 0; }
.stagger-list > :nth-child(2)  { --i: 1; }
.stagger-list > :nth-child(3)  { --i: 2; }
.stagger-list > :nth-child(4)  { --i: 3; }
.stagger-list > :nth-child(5)  { --i: 4; }
.stagger-list > :nth-child(6)  { --i: 5; }
.stagger-list > :nth-child(7)  { --i: 6; }
.stagger-list > :nth-child(8)  { --i: 7; }
.stagger-list > :nth-child(9)  { --i: 8; }
.stagger-list > :nth-child(10) { --i: 9; }

Or set the index with inline styles in your HTML or template: style="--i: 3". This is cleaner for dynamic lists.

Recipe 22: Responsive Breakpoint Variables

You cannot use variables inside @media conditions, but you can change variables inside media queries to centralize responsive logic:

:root {
    --layout: stack;
    --cols: 1;
    --heading-size: 1.5rem;
    --sidebar-visible: none;
    --content-max: 100%;
}

@media (min-width: 768px) {
    :root {
        --layout: row;
        --cols: 2;
        --heading-size: 2rem;
        --sidebar-visible: block;
        --content-max: 65ch;
    }
}

@media (min-width: 1200px) {
    :root {
        --cols: 3;
        --heading-size: 2.5rem;
        --content-max: 75ch;
    }
}

/* Components consume variables — zero media queries in components */
.page { display: flex; flex-direction: var(--layout); }
.sidebar { display: var(--sidebar-visible); }
.content { max-width: var(--content-max); }
h1 { font-size: var(--heading-size); }

Performance Considerations

CSS variables are fast, but a few patterns deserve attention:

/* GOOD: scoped to element */
card.style.setProperty('--tilt-x', x + 'deg');

/* LESS IDEAL: global, triggers full recalc */
document.documentElement.style.setProperty('--tilt-x', x + 'deg');

/* GOOD: batch updates */
requestAnimationFrame(() => {
    root.style.setProperty('--x', x + 'px');
    root.style.setProperty('--y', y + 'px');
    root.style.setProperty('--scale', s);
});

Browser Support and Fallbacks

CSS custom properties have over 98% global browser support (all browsers since 2017). The only browser that never supported them is Internet Explorer. You can safely use custom properties in any project without fallbacks.

The @property rule (needed for animated variables) is supported in Chrome 85+, Firefox 128+, Safari 15.4+, and Edge 85+ — all current browsers as of 2026.

If you must support ancient browsers, provide a static fallback before the variable declaration:

.btn {
    background: #3b82f6;                /* Fallback for IE */
    background: var(--primary, #3b82f6); /* Modern browsers */
}

For more CSS layout techniques, see our CSS Layout Techniques Guide and CSS Gradients Guide.

Frequently Asked Questions

How do I create a dark mode toggle using CSS variables?

Define all theme-dependent colors as CSS custom properties on :root for the light theme. Then create an override block using a class or data attribute (e.g., [data-theme="dark"]) that resets those same properties to dark values. Toggle the attribute with JavaScript: document.documentElement.setAttribute('data-theme', 'dark'). Save the user's preference in localStorage and restore it on page load. Combine with @media (prefers-color-scheme: dark) to respect system preferences when no manual choice has been made. See Recipe 1 above for the complete implementation.

Can CSS variables be animated or transitioned?

By default, CSS custom properties cannot be transitioned because the browser treats them as discrete (non-interpolatable) values. However, using the @property at-rule to register a custom property with a specific syntax (such as <color>, <length>, or <number>), you tell the browser the property's type, which enables smooth transitions and keyframe animations. See Recipe 8 for animated gradients and Recipe 16 for animated counters.

What is the best way to build a responsive spacing system with CSS variables?

Define a single --space-unit variable using clamp() for fluid scaling, then derive all spacing values from it using calc() with multipliers. This gives you a consistent scale (4px, 8px, 12px, 16px, 24px, etc.) that fluidly adapts to viewport size without breakpoints. Every component references the spacing tokens, so changing the unit updates the entire system proportionally. See Recipe 4 for the complete implementation.

How do I generate a full color palette from a single CSS variable?

Use HSL color mode with CSS variables. Define --hue as a single number (e.g., 220 for blue), then generate an entire 10-shade palette by varying the saturation and lightness values. Changing just the --hue variable regenerates every shade. You can scope the hue to sections of the page for multi-color themes. See Recipe 2 for the complete pattern.

How do CSS variables interact with calc() for dynamic layouts?

CSS variables and calc() are natural partners. Store base values and multipliers as custom properties, then combine them with calc() for computed results. This works for grid columns, sidebar widths, responsive font sizes, and any value that needs to be mathematically derived. Change one variable and every calculation using it updates automatically. See Recipe 7 for a layout example and Recipe 3 for a typography scale.

What are CSS variable performance best practices?

Scope JavaScript-driven property updates to the most specific element possible rather than :root. Keep fallback chains to 2-3 levels. Use @property registration for animated values. Batch multiple setProperty() calls inside a single requestAnimationFrame. A design token system with 50-200 properties on :root has negligible overhead. The main bottleneck is frequent setProperty() calls combined with properties that affect layout.

Summary

These 22 recipes cover the most common patterns you will encounter when building with CSS custom properties. The key principles across all of them:

  1. Centralize responsive logic by changing variables in media queries rather than scattering breakpoints across components.
  2. Use the three-tier token system (primitives, semantics, components) for scalable design systems.
  3. Scope variables to components for clean overrides without specificity wars.
  4. Combine with calc() for mathematical relationships between values.
  5. Register with @property to unlock animations and type safety.
  6. Bridge CSS and JavaScript through setProperty() and getComputedStyle() rather than inline styles.

For the foundational theory behind these recipes, read our CSS Custom Properties Complete Guide. For layout patterns, explore the CSS Grid Guide and CSS Layout Techniques Guide. For visual effects, see our CSS Gradients Guide.

Related Resources

CSS Variables Complete Guide
In-depth theory: scope, inheritance, cascade, and more
CSS Grid Complete Guide
Master two-dimensional CSS layout
CSS Gradients Complete Guide
Linear, radial, and conic gradients in depth
CSS Layout Techniques Guide
Flexbox, Grid, and modern layout patterns
Color Palette Generator
Generate color schemes for CSS custom properties
CSS Beautifier
Format and prettify CSS code with custom properties