Difficulty: Intermediate
How do CSS custom properties work? How are they different from Sass variables?
CSS custom properties (variables) are declared with -- prefix and used with var() function.
Key differences from Sass variables: 1. CSS variables cascade and inherit through the DOM 2. CSS variables can be changed at runtime with JavaScript 3. CSS variables can be scoped to any selector (not just global) 4. CSS variables work with media queries dynamically 5. Sass variables are compiled away - they don't exist at runtime
CSS variables enable: - Theming (dark mode, brand colors) - Component-scoped values - Runtime style changes without JavaScript - Responsive design without duplicating properties
/* Declare on :root for global scope */
:root {
--color-primary: #3b82f6;
--color-text: #1f2937;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--radius: 0.5rem;
--font-sans: 'Inter', system-ui, sans-serif;
}
/* Use with var() */
.button {
background: var(--color-primary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
font-family: var(--font-sans);
}
/* Fallback value */
.card {
background: var(--card-bg, white);
/* Uses white if --card-bg is not defined */
}
/* Computed values */
.element {
--size: 4;
width: calc(var(--size) * 1rem); /* 4rem */
}
Variables declared on :root are available everywhere. var() accepts a fallback value as second argument. Use calc() for computed values.
/* Light theme (default) */
:root {
--bg: #ffffff;
--bg-surface: #f9fafb;
--text: #111827;
--text-muted: #6b7280;
--border: #e5e7eb;
--shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}
/* Dark theme */
.dark {
--bg: #111827;
--bg-surface: #1f2937;
--text: #f9fafb;
--text-muted: #9ca3af;
--border: #374151;
--shadow: 0 1px 3px rgb(0 0 0 / 0.4);
}
/* Components use variables */
body {
background: var(--bg);
color: var(--text);
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
/* Toggle with JS */
/* document.documentElement.classList.toggle('dark') */
Toggle the dark class on the html element, and every component updates instantly. No need to change individual element styles.
/* Variables scoped to a component */
.btn {
--btn-bg: #3b82f6;
--btn-color: white;
--btn-size: 0.875rem;
background: var(--btn-bg);
color: var(--btn-color);
font-size: var(--btn-size);
padding: 0.5em 1em;
}
/* Variants override scoped variables */
.btn--danger {
--btn-bg: #ef4444;
}
.btn--outline {
--btn-bg: transparent;
--btn-color: #3b82f6;
}
.btn--lg {
--btn-size: 1.125rem;
}
/* Change variables via JavaScript */
/* element.style.setProperty('--btn-bg', '#10b981') */
/* Responsive variables */
:root {
--container-padding: 1rem;
}
@media (min-width: 768px) {
:root {
--container-padding: 2rem;
}
}
Scoped variables make component variants clean - just override the variable instead of repeating all the properties.
Custom Properties, CSS Variables, Theming, Cascading, Dynamic Styles