CSS Variables (Custom Properties): Complete Guide
Master CSS custom properties for maintainable, dynamic stylesheets.
What Are CSS Custom Properties?
CSS custom properties — commonly called CSS variables — are user-defined values you declare once and reuse throughout your stylesheet. Unlike Sass or Less variables that get compiled away before reaching the browser, CSS custom properties exist at runtime. That distinction matters: you can read them, update them, and respond to them with JavaScript, making them a core tool for dynamic theming and interactive interfaces.
The syntax is straightforward. Declare a property with a -- prefix, then reference it with the var() function:
:root {
--primary-color: #3498db;
--base-font-size: 16px;
--spacing-md: 1rem;
--border-radius: 4px;
}
body {
font-size: var(--base-font-size);
color: var(--primary-color);
}
.button {
background-color: var(--primary-color);
padding: var(--spacing-md) calc(var(--spacing-md) * 2);
border-radius: var(--border-radius);
font-size: var(--base-font-size);
}
This guide walks you through everything from scoping and fallbacks to JavaScript integration and the newer @property at-rule.
Declaration and Scope
One of the most powerful features of CSS custom properties is that you can declare them on any selector, not just :root. Properties cascade down to descendants, so a variable defined on a parent applies to all its children unless overridden.
/* Global defaults */
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--card-padding: 1.5rem;
}
/* Component-level override */
.sidebar {
--bg-color: #f4f4f4;
--card-padding: 1rem;
}
/* Dark section — overrides both global and component */
.dark-section {
--bg-color: #1a1a1a;
--text-color: #eeeeee;
}
/* These elements automatically use whatever --bg-color is in scope */
.card {
background-color: var(--bg-color);
color: var(--text-color);
padding: var(--card-padding);
}
Inside .sidebar, every .card picks up the lighter background and tighter padding. Inside .dark-section, the dark values apply — no extra selectors, no specificity fights.
A few rules to remember:
- Custom properties are case-sensitive.
--primary-colorand--Primary-Colorare two different properties. - A property declared inside
.sidebaris not accessible outside that element's subtree. - Valid names use letters, numbers, hyphens, and underscores after the
--prefix.
Fallback Values
The var() function accepts an optional second argument as a fallback value. The browser uses it when the referenced property is undefined — not as a browser compatibility shim, but as a genuine runtime safety net:
.card {
background-color: var(--card-bg, #f9f9f9);
border: var(--card-border, 1px solid #e0e0e0);
color: var(--card-text, inherit);
}
/* Fallbacks can even reference other custom properties */
.hero {
background-color: var(--hero-bg, var(--primary-color, #3498db));
}
This makes fallbacks composable. If --hero-bg isn't set, the browser tries --primary-color, and if that's also missing, it falls back to the literal hex value.
A smart pattern for design systems is separating user-facing variables from internal ones:
:root {
/* Users can set --user-accent to override the theme */
--accent-color: var(--user-accent, #d33a2c);
}
.button-primary {
background-color: var(--accent-color);
}
Now consumers of your component can set --user-accent on a parent element without touching your internal variable naming.
Dynamic Theming with JavaScript
This is where CSS variables genuinely outperform preprocessors. You can read and write custom properties at runtime using the CSSOM:
const root = document.documentElement;
// Read a custom property
const primaryColor = getComputedStyle(root)
.getPropertyValue('--primary-color')
.trim();
console.log(primaryColor); // "#3498db"
// Update a custom property globally
root.style.setProperty('--primary-color', '#e74c3c');
// Update on a specific element
const card = document.querySelector('.card');
card.style.setProperty('--card-bg', '#fff3cd');
// Remove an override (reverts to the cascade value)
root.style.removeProperty('--primary-color');
Here's a complete dark mode toggle that requires no class juggling beyond a single body class:
:root {
--bg: #ffffff;
--text: #1a1a1a;
--surface: #f4f4f4;
--primary: #3498db;
}
body.dark-mode {
--bg: #121212;
--text: #e0e0e0;
--surface: #1e1e1e;
--primary: #5dade2;
}
body {
background-color: var(--bg);
color: var(--text);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background-color: var(--surface);
color: var(--text);
}
.button {
background-color: var(--primary);
color: #fff;
}
const toggle = document.querySelector('#theme-toggle');
toggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
// Persist preference
const isDark = document.body.classList.contains('dark-mode');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
// Restore preference on load
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
One practical note: avoid updating custom properties on high-frequency events like scroll or mousemove. Each update can trigger layout recalculations. Throttle those handlers or use CSS @keyframes where possible.
The @property At-Rule
The @property at-rule lets you formally register a custom property, specifying its type, whether it inherits, and its initial value. The main practical benefit is enabling animated transitions on custom properties — something browsers can't do with unregistered variables because they don't know how to interpolate between two arbitrary strings.
@property --gradient-angle {
syntax: "<angle>";
initial-value: 45deg;
inherits: false;
}
@property --highlight-opacity {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
.card {
--gradient-angle: 45deg;
background: linear-gradient(
var(--gradient-angle),
#ff6b6b,
#4ecdc4
);
transition: --gradient-angle 0.6s ease;
}
.card:hover {
--gradient-angle: 225deg;
}
Without @property, that transition would snap instantly rather than animating, because the browser treats the unregistered variable as an opaque string. With a registered <angle> type, it knows how to interpolate degrees.
You can use this same technique for animating colors, lengths, and numbers — check our gradient generator if you want to experiment with animated gradient backgrounds visually.
Practical Patterns
Responsive Component Sizing
.button {
--btn-padding-y: 0.5rem;
--btn-padding-x: 1rem;
--btn-font-size: 0.875rem;
padding: var(--btn-padding-y) var(--btn-padding-x);
font-size: var(--btn-font-size);
border-radius: 4px;
}
.button--large {
--btn-padding-y: 0.75rem;
--btn-padding-x: 1.5rem;
--btn-font-size: 1rem;
}
@media (min-width: 768px) {
.button--large {
--btn-padding-y: 1rem;
--btn-padding-x: 2rem;
--btn-font-size: 1.125rem;
}
}
Spacing Scale
:root {
--space-unit: 0.25rem;
--space-1: calc(var(--space-unit) * 1); /* 4px */
--space-2: calc(var(--space-unit) * 2); /* 8px */
--space-4: calc(var(--space-unit) * 4); /* 16px */
--space-8: calc(var(--space-unit) * 8); /* 32px */
--space-16: calc(var(--space-unit) * 16); /* 64px */
}
Change --space-unit and your entire spacing system scales. You can combine this with CSS formatter to keep generated stylesheets clean and readable.
Common Mistakes to Avoid
Replacing all Sass variables blindly. CSS custom properties earn their keep when values need to change based on DOM state — hover, media queries, JavaScript interaction. For static brand colors that never change at runtime, a preprocessor variable is perfectly fine.
Dumping everything on :root. If a variable only applies to one component, scope it to that component. Globally declared properties that are only used locally add unnecessary overhead.
Ignoring fallbacks on critical properties. If --primary-color is undefined and you have no fallback, your button has no background color. Add sensible defaults for anything that affects layout or usability.
Forgetting the .trim() when reading with JavaScript. getPropertyValue often returns a value with leading whitespace. Always call .trim() before using the result.
You can also use the color picker when prototyping your color system to quickly find and copy values in the right format for your custom properties.
Naming Conventions
Consistent naming pays dividends when your stylesheet grows. One practical convention:
:root {
/* Tier 1: Raw values */
--color-blue-500: #3498db;
--color-blue-700: #2176ae;
/* Tier 2: Semantic roles — reference raw values */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-700);
/* Tier 3: Component tokens — reference semantic roles */
--button-bg: var(--color-primary);
--button-bg-hover: var(--color-primary-hover);
}
When you rebrand from blue to green, you change one line in Tier 1. Everything downstream updates automatically.
Next Steps
Now that you understand CSS custom properties, these topics build naturally on what you've learned:
- CSS
calc()and math functions — Combine with custom properties for fluid typography and spacing systems usingclamp()andmin()/max() - CSS Grid and Flexbox — Use custom properties to control grid column counts and gap sizes responsively
- Design token workflows — Export tokens from Figma or other tools and transform them directly into CSS custom properties
- CSS animations and
@keyframes— Pair with registered@propertytypes for smooth interpolated animations
Tools to use while practicing:
- CSS Formatter — Clean up and organize stylesheets as your variable declarations grow
- Gradient Generator — Experiment with animated gradients using
@property-registered angles and colors - Color Picker — Build your color token system with precise hex, HSL, and RGB values
Related Tools
CSS Formatter
Beautify and format CSS code with proper indentation and structure.
Gradient Generator
Create beautiful CSS gradients. Linear, radial, and conic with multiple color stops.
Color Picker
Visual color picker with various formats. Pick from screen or input values.
Color Palette Generator
Generate harmonious color palettes. Export as CSS variables, Tailwind, or SCSS.
Continue Learning
CSS Fundamentals: Styling Web Pages
Master CSS basics including selectors, properties, and the box model.
intermediateCSS Animations and Transitions
Create smooth animations and transitions with pure CSS.
beginnerCSS Media Queries: Complete Guide to Responsive Design
Master CSS media queries for building truly responsive layouts across all screen sizes.