Custom Properties Basics
CSS custom properties (also called CSS variables) are properties you define yourself, prefixed with --. They store values that can be referenced anywhere in your CSS using the var() function. Unlike preprocessor variables (Sass, Less), CSS custom properties are live — they exist in the DOM, can be changed with JavaScript, and respond to media queries and pseudo-classes at runtime.
/* Define on :root — globally accessible */
:root {
--color-primary: #7c6fff;
--color-secondary: #ff6fb0;
--spacing-md: 16px;
--radius-card: 12px;
--font-sans: 'Syne', sans-serif;
}
/* Use with var() */
.btn {
background: var(--color-primary);
padding: var(--spacing-md);
border-radius: var(--radius-card);
font-family: var(--font-sans);
}
/* Fallback value */
.element {
color: var(--color-text, #1a1a2e); /* uses #1a1a2e if --color-text is not set */
}
Cascading and Scoping
CSS custom properties follow the same cascade and inheritance rules as standard CSS properties. A property defined on a parent element is inherited by all its descendants. This lets you scope variables to specific components without affecting the rest of the page:
/* Global defaults */
:root {
--btn-bg: #7c6fff;
--btn-color: #fff;
}
/* Scoped override — only affects buttons inside .dark-section */
.dark-section {
--btn-bg: #1c1c28;
--btn-color: #7c6fff;
}
/* This button uses scoped values inside .dark-section */
.btn {
background: var(--btn-bg);
color: var(--btn-color);
}
/* Component-level variables */
.card {
--card-padding: 24px;
--card-radius: 16px;
padding: var(--card-padding);
border-radius: var(--card-radius);
}
/* Compact variant — override at component level */
.card.compact {
--card-padding: 12px;
--card-radius: 8px;
}
Dynamic Theming
Because custom properties are live DOM values, toggling themes is as simple as swapping a class on the root element. No JavaScript bundling, no style recalculation beyond what the browser already does — themes switch instantly:
/* Base (light) theme */
:root {
--bg: #f5f5f8;
--surface: #ffffff;
--text: #1a1a2e;
--muted: #6a6a8a;
--accent: #7c6fff;
}
/* Dark theme */
:root.dark {
--bg: #0a0a0f;
--surface: #13131a;
--text: #f0f0ff;
--muted: #7070a0;
--accent: #7c6fff;
}
/* System preference — automatic */
@media (prefers-color-scheme: dark) {
:root {
--bg: #0a0a0f;
--surface: #13131a;
--text: #f0f0ff;
--muted: #7070a0;
}
}
/* Toggle with JavaScript */
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
// Restore on page load
const saved = localStorage.getItem('theme');
if (saved === 'dark') document.documentElement.classList.add('dark');
Design Tokens with CSS Variables
Design tokens are named values that represent design decisions — spacing, colour, type scale, radius. CSS custom properties are the ideal implementation for tokens because they are native to the browser, have no build step, and can be overridden at any level of the cascade.
:root {
/* Colour palette */
--color-purple-50: #EEEDFE;
--color-purple-500: #7c6fff;
--color-purple-900: #26215C;
/* Semantic colours */
--color-accent: var(--color-purple-500);
--color-text-primary: #1a1a2e;
--color-text-muted: #6a6a8a;
--color-surface: #ffffff;
--color-border: #d8d8e4;
/* Spacing scale (base 4) */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
/* Type scale */
--text-xs: 0.72rem;
--text-sm: 0.82rem;
--text-base: 0.92rem;
--text-lg: 1.1rem;
--text-xl: 1.4rem;
--text-2xl: 1.8rem;
/* Border radius */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.04);
--shadow-md: 0 2px 4px rgba(0,0,0,0.04), 0 6px 16px rgba(0,0,0,0.06);
--shadow-lg: 0 4px 8px rgba(0,0,0,0.04), 0 12px 32px rgba(0,0,0,0.08);
}
Interacting with JavaScript
CSS custom properties are fully readable and writable from JavaScript through the CSSOM. This enables dynamic animations, user-controlled themes, and component state reflected as CSS variables:
// Read a CSS variable
const root = document.documentElement;
const accent = getComputedStyle(root).getPropertyValue('--color-accent').trim();
console.log(accent); // '#7c6fff'
// Set a CSS variable
root.style.setProperty('--color-accent', '#ff6fb0');
// Set on a specific element
const card = document.querySelector('.card');
card.style.setProperty('--card-padding', '8px');
// Animate via CSS variable (requires @property for transitions)
// Example: mouse-position-aware gradient
document.addEventListener('mousemove', (e) => {
const x = (e.clientX / window.innerWidth * 100).toFixed(1);
const y = (e.clientY / window.innerHeight * 100).toFixed(1);
root.style.setProperty('--mouse-x', x + '%');
root.style.setProperty('--mouse-y', y + '%');
});
/* Mouse-following radial gradient using JS-set variables */
.hero {
background: radial-gradient(
circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
rgba(124, 111, 255, 0.3),
transparent 40%
),
#0a0a1a;
}
Practical Tips
- Name semantically, not by value —
--color-accentis better than--color-purple. When you change the accent colour, you only update one place. - Use a two-level system — primitive tokens (
--color-purple-500) and semantic tokens (--color-accent: var(--color-purple-500)). Theming only requires changing semantic tokens. - Document with comments — group related tokens and add a comment explaining the scale or intent. Future maintainers will thank you.
- Use @property for typed variables — the
@propertyat-rule lets you define the type, initial value, and inheritance of a custom property. This unlocks native CSS transitions on custom properties.
/* @property enables transitions on custom properties */
@property --gradient-angle {
syntax: '';
initial-value: 0deg;
inherits: false;
}
.rotating-gradient {
background: linear-gradient(var(--gradient-angle), #7c6fff, #ff6fb0);
animation: rotate 4s linear infinite;
}
@keyframes rotate {
to { --gradient-angle: 360deg; }
}
Generate the values that fill your design tokens
Gradients, shadows, border radii, and button styles — all ready to paste as CSS variable values.
Start with the Gradient Generator →