CSS Custom Properties (Variables) Complete Guide: Theming and Dynamic Styles
CSS custom properties (commonly called "CSS variables") are one of the most powerful features in modern CSS. They enable theming, dynamic values, and significantly reduce repetition.
Basic Syntax
/* Define on :root (global scope) */
:root {
--color-primary: #3b82f6;
--color-secondary: #6366f1;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--border-radius: 8px;
--font-size-base: 16px;
--font-family: 'Inter', system-ui, sans-serif;
}
/* Use with var() */
.button {
background: var(--color-primary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
font-family: var(--font-family);
}
Fallback Values
/* var(--property, fallback) */
.card {
background: var(--card-bg, white); /* white if --card-bg not defined */
padding: var(--card-padding, 24px);
/* Nested fallbacks */
color: var(--text-color, var(--color-neutral-900, #111));
}
Scope and Inheritance
/* Global (available everywhere) */
:root {
--font-size: 16px;
}
/* Component-scoped */
.card {
--card-padding: 24px;
--card-border: 1px solid #e5e7eb;
padding: var(--card-padding);
border: var(--card-border);
}
/* Nested: overrides parent scope */
.card.card--compact {
--card-padding: 12px; /* Only affects .card.card--compact and its children */
}
Dark Mode Theming
/* Light mode (default) */
:root {
--color-bg: #ffffff;
--color-bg-secondary: #f9fafb;
--color-text: #111827;
--color-text-secondary: #6b7280;
--color-border: #e5e7eb;
--color-primary: #3b82f6;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111827;
--color-bg-secondary: #1f2937;
--color-text: #f9fafb;
--color-text-secondary: #9ca3af;
--color-border: #374151;
--color-primary: #60a5fa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
}
/* Or with a class (toggled by JavaScript) */
[data-theme="dark"] {
--color-bg: #111827;
--color-text: #f9fafb;
/* ... */
}
// Toggle dark mode
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
document.documentElement.setAttribute('data-theme', isDark ? 'light' : 'dark');
localStorage.setItem('theme', isDark ? 'light' : 'dark');
});
// Restore on page load
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
Design Token System
:root {
/* ── Primitives (raw values) ── */
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-900: #1e3a8a;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-500: #6b7280;
--gray-900: #111827;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
/* ── Semantic tokens (use these in components) ── */
--color-primary: var(--blue-500);
--color-primary-hover: var(--blue-600);
--color-text: var(--gray-900);
--color-text-muted: var(--gray-500);
--color-bg: white;
--color-bg-subtle: var(--gray-50);
--color-border: var(--gray-100);
--spacing-component: var(--space-4);
--spacing-section: var(--space-8);
/* ── Component tokens ── */
--button-padding: var(--space-2) var(--space-4);
--card-padding: var(--space-6);
--input-height: 40px;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
}
JavaScript Integration
// Read CSS variable value
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--color-primary').trim();
console.log(primaryColor); // '#3b82f6'
// Set CSS variable from JavaScript
root.style.setProperty('--color-primary', '#ef4444');
// Dynamic theming
function applyTheme(colors) {
Object.entries(colors).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value);
});
}
applyTheme({ primary: '#8b5cf6', secondary: '#ec4899' });
// Animate with CSS variables
let hue = 0;
function animateHue() {
root.style.setProperty('--hue', hue++);
if (hue > 360) hue = 0;
requestAnimationFrame(animateHue);
}
Responsive Values
:root {
--container-padding: 16px;
--font-size-h1: 2rem;
--columns: 1;
}
@media (min-width: 640px) {
:root {
--container-padding: 24px;
--font-size-h1: 2.5rem;
--columns: 2;
}
}
@media (min-width: 1024px) {
:root {
--container-padding: 48px;
--font-size-h1: 3rem;
--columns: 3;
}
}
.container { padding: 0 var(--container-padding); }
h1 { font-size: var(--font-size-h1); }
.grid { grid-template-columns: repeat(var(--columns), 1fr); }
CSS Variables with calc()
:root {
--base-size: 8px;
--sidebar-width: 260px;
}
.grid {
grid-template-columns: var(--sidebar-width) 1fr;
}
/* Responsive sidebar that gets narrower on tablets */
.content {
max-width: calc(100% - var(--sidebar-width) - 32px);
}
/* Scale typography */
h1 { font-size: calc(var(--base-size) * 4); } /* 32px */
h2 { font-size: calc(var(--base-size) * 3); } /* 24px */
p { font-size: calc(var(--base-size) * 2); } /* 16px */
Component Pattern
/* Button component with customizable variables */
.btn {
/* Component defaults */
--btn-bg: var(--color-primary);
--btn-text: white;
--btn-padding: 0.5rem 1rem;
--btn-radius: var(--border-radius-md);
--btn-font-size: 0.875rem;
/* Use variables */
background: var(--btn-bg);
color: var(--btn-text);
padding: var(--btn-padding);
border-radius: var(--btn-radius);
font-size: var(--btn-font-size);
border: none;
cursor: pointer;
}
/* Variant: Override just the variables */
.btn-danger { --btn-bg: #ef4444; }
.btn-success { --btn-bg: #22c55e; }
.btn-outline {
--btn-bg: transparent;
--btn-text: var(--color-primary);
border: 1px solid var(--color-primary);
}
/* Size variant */
.btn-lg { --btn-padding: 0.75rem 1.5rem; --btn-font-size: 1rem; }
.btn-sm { --btn-padding: 0.25rem 0.5rem; --btn-font-size: 0.75rem; }
What CSS Variables Can't Do
/* ❌ Can't use in media query values */
:root { --breakpoint-md: 768px; }
@media (min-width: var(--breakpoint-md)) { } /* Doesn't work! */
/* ❌ Can't use as property name */
.item { var(--property): red; } /* Doesn't work! */
/* ✅ Workaround: use @custom-media (future spec) or JS */
→ Convert and explore color formats with the Color Converter.