CSS Custom Properties (CSS Variables)
"CSS Custom Properties are runtime variables. They live in the browser's CSSOM and can be changed, inherited, or overridden dynamically — something Sass variables will never be able to do."
Martin Metodiev, Design Systems Collective
The Moment Everything Changes
CSS Variables vs Sass Variables
| Feature | Sass Variables | CSS Custom Properties |
|---|---|---|
| Scope | Compile-time only | Runtime (browser) |
| Dynamic updates | ❌ Impossible | ✅ Yes — via JS |
| Cascade & inheritance | ❌ No | ✅ Yes — like any CSS property |
| Media query reactivity | ❌ No | ✅ Yes |
| JS interaction | ❌ No | ✅ Yes |
| DevTools visibility | ❌ Compiled away | ✅ Visible, editable |
"Sass variables are compile-time constants — they disappear once Sass compiles to CSS. CSS Custom Properties are runtime variables. Conflating these two concepts is dangerous."
Use Sass variables for
Use CSS custom properties for
The MDN Reference
MDN Web Docs — Using CSS Custom Properties (Variables):
Building a Token System from Scratch
The most powerful pattern is the two-tier token system: primitive tokens → semantic tokens → components.
Primitive Tokens (Raw Values)
/* tokens/primitives.css */
:root {
/* Color palette — raw values */
--color-blue-50: #eff6ff;
--color-blue-100: #dbeafe;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-blue-700: #1d4ed8;
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-500: #6b7280;
--color-gray-900: #111827;
/* Spacing scale */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-12: 3rem; /* 48px */
/* Border radius */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-full: 9999px;
}
Semantic Tokens (Contextual Meaning)
/* tokens/semantic.css — light theme (default) */
:root {
--color-background: var(--color-gray-50);
--color-surface: white;
--color-surface-elevated: white;
--color-border: var(--color-gray-100);
--color-text-primary: var(--color-gray-900);
--color-text-secondary: var(--color-gray-500);
--color-brand-primary: var(--color-blue-600);
--color-brand-hover: var(--color-blue-700);
--color-brand-subtle: var(--color-blue-50);
}
"Separate primitive and semantic tokens: keep raw color values (like `--color-blue-600`) separate from their semantic usage (like `--color-brand-primary`)."
FrontendTools, CSS Variables Guide 2027
Component Styles Using Tokens
/* components/button.css */
.btn-primary {
background-color: var(--color-brand-primary);
color: white;
padding: var(--space-3) var(--space-6);
border-radius: var(--radius-md);
font-weight: 500;
transition: background-color 150ms ease;
}
.btn-primary:hover {
background-color: var(--color-brand-hover);
}
Light / Dark Mode in 15 Lines
/* Dark theme — override semantic tokens */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--color-gray-900);
--color-surface: #1f2937;
--color-surface-elevated: #374151;
--color-border: #374151;
--color-text-primary: var(--color-gray-50);
--color-text-secondary: var(--color-gray-500);
--color-brand-primary: var(--color-blue-500);
--color-brand-hover: var(--color-blue-400);
--color-brand-subtle: #1e3a5f;
}
}
And your JavaScript toggle:
// Toggle theme with one DOM attribute
const toggleTheme = () => {
const current = document.documentElement.getAttribute('data-theme');
document.documentElement.setAttribute(
'data-theme',
current === 'dark' ? 'light' : 'dark'
);
localStorage.setItem('theme', current === 'dark' ? 'light' : 'dark');
};
// On load — respect saved preference
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
Runtime Theming with JavaScript
// Read a CSS variable
const brandColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-brand-primary').trim();
// Write a CSS variable — live theme customisation
document.documentElement.style.setProperty('--color-brand-primary', '#10b981');
document.documentElement.style.setProperty('--color-brand-hover', '#059669');
Typed Custom Properties with `@property`
/* Define a typed custom property — enables CSS transitions on the value */
@property --brand-hue {
syntax: '<number>';
inherits: true;
initial-value: 220;
}
:root {
--brand-hue: 220;
--color-brand: hsl(var(--brand-hue) 85% 55%);
}
/* Now you can animate the hue shift */
.logo:hover {
--brand-hue: 270;
transition: --brand-hue 400ms ease;
}
Responsive Fluid Typography with CSS Variables
:root {
/* Fluid type scale — scales between viewport widths */
--text-sm: clamp(0.875rem, 0.8rem + 0.3vw, 1rem);
--text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--text-lg: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
--text-xl: clamp(1.25rem, 1.1rem + 0.8vw, 1.75rem);
--text-2xl: clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem);
--text-4xl: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-2xl); }
p { font-size: var(--text-base); line-height: 1.6; }
For Business Leaders
"Teams that simplify CSS with custom properties ship faster, reduce regressions, and make designers and developers smile." Brand Nexus Studios, 2027
Explore project snapshots or discuss custom web solutions.
Simplicity is not the absence of clutter, that's a consequence of simplicity. Simplicity is somehow essentially describing the purpose and place of an object and product.
Thank You for Spending Your Valuable Time
I truly appreciate you taking the time to read blog. Your valuable time means a lot to me, and I hope you found the content insightful and engaging!
Frequently Asked Questions
Yes. CSS custom properties have had excellent browser support since 2016 (Chrome 49, Firefox 31, Safari 9.1). The `@property` at-rule has slightly lower but growing support — check for current status.
Gradually, for new code. For values that only matter at build time (breakpoint numbers used in `@media`, build-time loop variables), Sass is still appropriate. For anything visual that might need runtime theming or dynamic behaviour, CSS custom properties are the right choice.
Use the second argument of `var()`: `color: var(--color-brand-primary, #2563eb)`. The fallback is used if the property is undefined. You can also chain: `var(--component-color, var(--color-brand-primary, #2563eb))`.
You can use them as *values* inside media queries, but you cannot use them as the *condition* of a media query (e.g., `@media (min-width: var(--breakpoint-lg))` doesn't work — this is a known limitation). Use Sass variables or hardcoded values for media query thresholds.
Dramatically disseminate real-time portals rather than top-line action items. Uniquely provide access to low-risk high-yield products without dynamic products. Progressively re-engineer low-risk high-yield ideas rather than emerging alignments.
Follow a consistent pattern: `--[category]-[variant]-[state]`. Example: `--color-text-primary`, `--color-text-disabled`, `--space-4`, `--radius-md`. Avoid generic names like `--blue` or `--primary` — semantic names communicate usage, not just values.
Comments are closed