Skip to content

Custom Properties

CSS Custom Properties (CSS Variables) provide native variable support that works at runtime, unlike preprocessor variables that are compiled away.

// Sass variables
$primary-color: #3b82f6;
$secondary-color: #10b981;
$spacing-unit: 8px;
$border-radius: 4px;
.button {
background-color: $primary-color;
padding: $spacing-unit * 2;
border-radius: $border-radius;
}
.button-secondary {
background-color: $secondary-color;
}
// Theme switching requires:
// 1. Separate compiled stylesheets
// 2. JavaScript to swap stylesheets
// 3. Or duplicated rule sets

Problems:

  • Variables compiled away at build time
  • Cannot change values at runtime
  • Theme switching requires multiple stylesheets or duplicated code
  • No JavaScript access to variable values
  • No cascade or inheritance
:root {
--color-primary: #3b82f6;
--color-secondary: #10b981;
--spacing-unit: 8px;
--border-radius: 4px;
}
.button {
background-color: var(--color-primary);
padding: calc(var(--spacing-unit) * 2);
border-radius: var(--border-radius);
}
.button-secondary {
background-color: var(--color-secondary);
}
/* Theme switching - just update the variables */
[data-theme='dark'] {
--color-primary: #60a5fa;
--color-secondary: #34d399;
}

Benefits:

  • Runtime variable updates
  • JavaScript can read and modify values
  • Cascade and inheritance work naturally
  • Single stylesheet for multiple themes
  • Fallback values built-in
.element {
/* Fallback if --color-accent is not defined */
color: var(--color-accent, #3b82f6);
/* Nested fallbacks */
background: var(--bg-custom, var(--bg-default, white));
}
.card {
--card-padding: 16px;
--card-radius: 8px;
padding: var(--card-padding);
border-radius: var(--card-radius);
}
.card.compact {
--card-padding: 8px;
--card-radius: 4px;
/* No need to redeclare the properties */
}
// Read a CSS variable
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue(
'--color-primary'
);
// Set a CSS variable
document.documentElement.style.setProperty('--color-primary', '#ff0000');
// Set on a specific element
element.style.setProperty('--local-var', '20px');
:root {
--base-size: 16px;
--scale-ratio: 1.25;
}
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));
}
@property --gradient-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.animated-gradient {
background: linear-gradient(var(--gradient-angle), #3b82f6, #10b981);
animation: rotate-gradient 3s linear infinite;
}
@keyframes rotate-gradient {
to {
--gradient-angle: 360deg;
}
}

Supported in all modern browsers. See caniuse.com/css-variables.

@property for typed custom properties has good support. See caniuse.com/mdn-css_at-rules_property.

  • Custom properties are resolved at computed-value time
  • Changes trigger style recalculation for affected elements
  • Scope variables as narrowly as possible for best performance
  • Avoid setting custom properties in tight loops

Use Custom Properties when:

  • Building theme systems (light/dark mode)
  • Values need to change at runtime
  • JavaScript needs to read or modify CSS values
  • Creating component-scoped design tokens
  • Building responsive systems with calculated values

Keep using preprocessor variables when:

  • Values are truly static and never change
  • You need preprocessing features (mixins, functions)
  • Working with legacy browser requirements