CSS Custom Properties (Variables): The Complete Guide

February 11, 2026 · 8 min read · CSS

CSS custom properties — commonly called CSS variables — have revolutionized how we write and maintain stylesheets. They allow you to define reusable values that cascade through your CSS, making theming, dark mode implementation, and responsive design dramatically easier. This comprehensive guide covers everything from basic syntax to advanced patterns used by professional developers.

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 and accessed using the var() function. Unlike preprocessor variables (Sass, Less), CSS custom properties are live — they cascade, inherit, and can be manipulated with JavaScript at runtime.

:root {
  --primary-color: #6366f1;
  --font-size-base: 16px;
  --spacing-unit: 8px;
}

.button {
  background: var(--primary-color);
  font-size: var(--font-size-base);
  padding: calc(var(--spacing-unit) * 2);
}

The :root selector targets the document's root element (<html>), making variables globally available. However, you can define custom properties on any selector for scoped values.

Basic Syntax and Usage

Defining a custom property requires the double-dash prefix. The property name is case-sensitive, so --color and --Color are different variables.

/* Define variables */
:root {
  --brand-blue: #3b82f6;
  --brand-red: #ef4444;
  --radius: 8px;
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

/* Use variables */
.card {
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* Fallback values */
.text {
  color: var(--text-color, #333);
  /* Uses #333 if --text-color is not defined */
}

The fallback value in var() is incredibly useful for component-level defaults. If the variable isn't defined in the current scope, the fallback kicks in.

Scoping and Inheritance

One of the most powerful features of CSS custom properties is their scoping behavior. Variables defined on a parent element are inherited by all children, but you can override them at any level.

.dark-section {
  --bg: #1a1a2e;
  --text: #e0e0e0;
}

.light-section {
  --bg: #ffffff;
  --text: #333333;
}

/* Same component, different themes */
.card {
  background: var(--bg);
  color: var(--text);
}

This scoping pattern is the foundation of component-based theming. The same .card component automatically adapts based on which section it's placed in — no extra CSS classes needed.

Building a Dark Mode with CSS Variables

CSS variables make dark mode implementation elegant and maintainable. Define your color tokens as variables, then override them based on a class or media query.

:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f5f5f5;
  --text-primary: #1a1a1a;
  --text-secondary: #666666;
  --border: #e0e0e0;
  --accent: #6366f1;
}

[data-theme="dark"] {
  --bg-primary: #0f0f1a;
  --bg-secondary: #1a1a2e;
  --text-primary: #e0e0e0;
  --text-secondary: #a0a0a0;
  --border: #2a2a3e;
  --accent: #818cf8;
}

/* Automatic dark mode via system preference */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-primary: #0f0f1a;
    --text-primary: #e0e0e0;
    /* ... */
  }
}

With this approach, you write your CSS once using variable references, and the entire color scheme changes by toggling a single attribute. Try our Color Converter to find the perfect color pairs for your theme.

Dynamic Values with calc()

CSS variables work seamlessly with calc(), enabling mathematical relationships between values. This is perfect for spacing systems and responsive typography.

:root {
  --base-size: 16px;
  --scale-ratio: 1.25;
  --space: 8px;
}

h1 { font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio)); }
h2 { font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio)); }
h3 { font-size: calc(var(--base-size) * var(--scale-ratio)); }

.container {
  padding: calc(var(--space) * 3);
  gap: calc(var(--space) * 2);
}

JavaScript Integration

Unlike Sass variables that compile away, CSS custom properties exist in the DOM and can be read and written with JavaScript. This opens up powerful dynamic styling possibilities.

// Read a CSS variable
const root = document.documentElement;
const primary = getComputedStyle(root).getPropertyValue('--primary-color');

// Set a CSS variable
root.style.setProperty('--primary-color', '#10b981');

// Use with user input (e.g., a color picker)
colorInput.addEventListener('input', (e) => {
  root.style.setProperty('--accent', e.target.value);
});

This is how many theme customization UIs work — the user picks colors, and JavaScript updates the CSS variables in real-time. The entire page updates instantly without reloading.

Responsive Design Patterns

CSS variables can be redefined inside media queries, making responsive adjustments clean and centralized.

:root {
  --columns: 1;
  --font-size: 14px;
  --container-padding: 16px;
}

@media (min-width: 768px) {
  :root {
    --columns: 2;
    --font-size: 16px;
    --container-padding: 24px;
  }
}

@media (min-width: 1200px) {
  :root {
    --columns: 3;
    --font-size: 18px;
    --container-padding: 32px;
  }
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--columns), 1fr);
  padding: var(--container-padding);
  font-size: var(--font-size);
}

Advanced Patterns: Component APIs

Modern CSS architecture uses custom properties as component APIs — defining what can be customized from the outside while keeping implementation details private.

/* Component defines its API */
.button {
  --btn-bg: var(--accent, #6366f1);
  --btn-color: var(--on-accent, white);
  --btn-radius: var(--radius, 8px);
  --btn-padding: 12px 24px;
  
  background: var(--btn-bg);
  color: var(--btn-color);
  border-radius: var(--btn-radius);
  padding: var(--btn-padding);
  border: none;
  cursor: pointer;
}

/* Consumer customizes via the API */
.hero .button {
  --btn-bg: #10b981;
  --btn-padding: 16px 32px;
}

Browser Support and Fallbacks

CSS custom properties are supported in all modern browsers (Chrome, Firefox, Safari, Edge) with over 97% global support. For the rare cases where you need fallback support:

/* Fallback for very old browsers */
.element {
  color: #6366f1; /* fallback */
  color: var(--primary, #6366f1);
}

/* Feature detection */
@supports (--custom: property) {
  .element {
    /* Use custom properties freely */
  }
}

Common Mistakes to Avoid

Real-World Example: Design System Tokens

Here's how a production design system might structure its CSS variables:

:root {
  /* Primitives (raw values) */
  --blue-500: #3b82f6;
  --gray-100: #f3f4f6;
  --gray-900: #111827;
  
  /* Semantic tokens (purpose-based) */
  --color-primary: var(--blue-500);
  --color-bg: var(--gray-100);
  --color-text: var(--gray-900);
  
  /* Component tokens (specific usage) */
  --button-bg: var(--color-primary);
  --card-bg: white;
  --nav-height: 64px;
}

This three-tier approach (primitives → semantic → component) creates a flexible, maintainable design system that scales beautifully. Tools like our CSS Gradient Generator and Color Converter can help you build these token palettes.

🎨 Build Your Color System

Use our Color Converter to generate complementary, triadic, and analogous palettes — then plug them into your CSS variables for a cohesive design system.

Conclusion

CSS custom properties have transformed modern CSS development. They enable dynamic theming, clean responsive design, component-based architecture, and JavaScript interactivity — all natively in CSS. Start using them today by defining a few key variables in your :root and replacing repeated values throughout your stylesheets. Your future self (and your team) will thank you.