CSS Custom Properties (Variables)

What You'll Learn

  • How to define and use custom properties (CSS variables)
  • The difference between :root and scoped variables
  • How to use fallback values for undefined variables
  • Creating dynamic themes by overriding variables in scope
  • Combining custom properties with calc() for powerful systems
  • Using @property to enable custom property animations
  • Integrating JavaScript with CSS variables for interactivity
  • Best practices for naming and organizing custom properties

Introduction to Custom Properties

Custom properties (CSS variables) are one of the most powerful features in modern CSS. Unlike preprocessor variables (Sass, Less), CSS custom properties are live values that cascade, inherit, and can be modified at runtime—making them perfect for theming, dynamic styling, and JavaScript integration.

They're defined using --property-name syntax and accessed with the var() function. Because they're native CSS, they update immediately when changed, enabling responsive, interactive designs without recompilation.

Basic Usage

Custom properties are typically defined in the :root selector for global scope, making them available throughout your entire stylesheet.

:root { --primary-color: #2196f3; --secondary-color: #ff9800; --spacing-unit: 1rem; --border-radius: 8px; --transition-speed: 0.3s; } .button { background-color: var(--primary-color); padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); border-radius: var(--border-radius); transition: transform var(--transition-speed); }

Fallback Values

The var() function accepts a second parameter: a fallback value used when the custom property isn't defined. This provides safe defaults and prevents broken styles.

.element { /* If --undefined-color doesn't exist, use #333 */ color: var(--undefined-color, #333); /* Fallback can be any value, even complex ones */ background: var(--bg-gradient, linear-gradient(135deg, #667eea, #764ba2)); /* Can nest var() with multiple fallbacks */ border-color: var(--border-1, var(--border-2, var(--border-default))); }

Scoped Variables and Cascade

Custom properties follow the cascade and can be redefined in any selector. This enables powerful component theming where you override variables within a specific scope.

.card { --card-bg: white; --card-text: #333; --card-border: #e0e0e0; background: var(--card-bg); color: var(--card-text); border: 2px solid var(--card-border); } .card.dark { /* Override only for this card */ --card-bg: #1a1a1a; --card-text: #e0e0e0; --card-border: #444; } .card.accent { /* Different overrides for accent cards */ --card-bg: #fff3e0; --card-text: #e65100; --card-border: #ffb74d; }

Theming with Custom Properties

One of the most practical uses of custom properties is building theme systems. By changing a few variables at a high level, you can transform an entire design.

.theme-container { --theme-bg: white; --theme-text: #333; --theme-border: #e0e0e0; --theme-accent: #2196f3; --theme-card-bg: #f8f9fa; background: var(--theme-bg); color: var(--theme-text); border: 2px solid var(--theme-border); } .theme-container.dark-mode { --theme-bg: #1a1a1a; --theme-text: #e0e0e0; --theme-border: #444; --theme-accent: #64b5f6; --theme-card-bg: #2d2d2d; } /* All child elements automatically update! */ .button { background: var(--theme-accent); } .card { background: var(--theme-card-bg); }

Variables with calc()

Combining custom properties with calc() creates powerful design systems where values are derived from base units. Change the base, and everything scales proportionally.

:root { --base-unit: 8; /* Now derive everything from this base */ } .element { margin: calc(var(--base-unit) * 2px); /* 16px */ padding: calc(var(--base-unit) * 1px); /* 8px */ gap: calc(var(--base-unit) * 3px); /* 24px */ } /* Responsive sizing with variables */ .box { --size: 100; width: calc(var(--size) * 1px); height: calc(var(--size) * 1px); } .box:hover { --size: 150; /* Just change the variable! */ }

Dynamic Gradients

Custom properties work with any CSS value, including complex ones like gradients. This enables dynamic, customizable backgrounds that can be adjusted with just a few variable changes.

.gradient-box { --gradient-start: #667eea; --gradient-end: #764ba2; --gradient-angle: 135deg; background: linear-gradient( var(--gradient-angle), var(--gradient-start), var(--gradient-end) ); } /* Create infinite variations */ .gradient-warm { --gradient-start: #f093fb; --gradient-end: #f5576c; --gradient-angle: 90deg; } .gradient-cool { --gradient-start: #4facfe; --gradient-end: #00f2fe; --gradient-angle: 180deg; }

Animated Custom Properties (@property)

The @property rule is a modern feature that lets you define the syntax, initial value, and inheritance of custom properties. This enables smooth animations of custom properties, which wasn't previously possible.

@property --animated-hue { syntax: ''; initial-value: 0; inherits: false; } .animated-box { background: hsl(var(--animated-hue), 70%, 50%); animation: hue-rotate 10s linear infinite; } @keyframes hue-rotate { to { --animated-hue: 360; } } /* Also works with angles */ @property --gradient-angle { syntax: ''; initial-value: 0deg; inherits: false; } .rotating-gradient { background: linear-gradient(var(--gradient-angle), #667eea, #764ba2); animation: rotate 5s linear infinite; } @keyframes rotate { to { --gradient-angle: 360deg; } }

JavaScript Integration

Custom properties can be read and modified via JavaScript, creating a powerful bridge between your styles and interactivity. This is perfect for user controls, dynamic theming, and responsive visualizations.

.box { --box-size: 100; --box-hue: 200; width: calc(var(--box-size) * 1px); height: calc(var(--box-size) * 1px); background: hsl(var(--box-hue), 70%, 50%); transition: all 0.3s ease; } // Set a custom property element.style.setProperty('--box-size', '200'); element.style.setProperty('--box-hue', '280'); // Read a custom property const size = getComputedStyle(element).getPropertyValue('--box-size'); // Set at document level for global changes document.documentElement.style.setProperty('--primary-color', '#ff0000');

Best Practices

Follow these guidelines to create maintainable, scalable custom property systems:

1. Use Semantic Names

Name variables based on their purpose, not their value. Use --primary-color instead of --blue, --spacing-large instead of --space-32.

2. Organize by Category

Group related variables together: colors, spacing, typography, shadows, etc. This makes them easier to find and maintain.

:root { /* Colors */ --primary-color: #2196f3; --secondary-color: #ff9800; --text-color: #333; /* Spacing */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 2rem; /* Typography */ --font-family-base: system-ui, sans-serif; --font-size-sm: 0.875rem; --font-size-base: 1rem; --line-height-base: 1.5; /* Effects */ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); --border-radius: 8px; --transition-speed: 0.3s; }

3. Provide Fallbacks

Always provide fallback values when using custom properties in contexts where they might not be defined:

.element { color: var(--text-color, #333); padding: var(--spacing, 1rem); }

4. Use calc() for Relationships

Derive values from base units using calc() to create proportional spacing systems and maintain mathematical relationships:

:root { --base-spacing: 1rem; } .element { padding: var(--base-spacing); margin: calc(var(--base-spacing) * 2); gap: calc(var(--base-spacing) * 0.5); }

5. Use @property for Animations

When you need to animate custom properties, use @property to define their syntax. This enables smooth transitions instead of instant changes.

6. Document Your Variables

Add comments explaining the purpose and usage of custom properties, especially for complex design systems:

:root { /* Primary brand color - used for CTAs, links, highlights */ --primary-color: #2196f3; /* Base spacing unit (8px) - all spacing derives from this */ --spacing-unit: 0.5rem; /* Z-index scale - use these instead of arbitrary values */ --z-dropdown: 1000; --z-modal: 2000; --z-tooltip: 3000; }

Key Takeaways