CSS Variables & Custom Properties
What are CSS variables?
CSS variables (officially called custom properties) let you store values and reuse them throughout your stylesheet. Instead of repeating the same color or spacing value in 50 places, you define it once and reference it everywhere.
CSS
:root {
--color-primary: #2563eb;
--color-text: #1e293b;
--spacing-md: 1rem;
--radius: 8px;
}
.button {
background: var(--color-primary);
color: white;
padding: var(--spacing-md);
border-radius: var(--radius);
}
.link {
color: var(--color-primary);
}Change
--color-primary in one place, and every button, link, and accent color updates.Defining variables
Variables are defined with
-- prefix and used with var():CSS
:root {
--font-sans: system-ui, -apple-system, sans-serif;
--max-width: 1200px;
}
body {
font-family: var(--font-sans);
max-width: var(--max-width);
}:root targets the <html> element and makes variables available globally. You can also define them on any element to scope them.Fallback values
If a variable isn't defined,
var() accepts a fallback:CSS
.card {
color: var(--card-text, #333);
padding: var(--card-padding, 1rem);
}Scoped variables
Variables cascade like any CSS property. Define them on a specific element to override the global value:
CSS
:root {
--bg: #ffffff;
--text: #1e293b;
}
.dark-section {
--bg: #0f172a;
--text: #f1f5f9;
}
.card {
background: var(--bg);
color: var(--text);
}A
.card inside .dark-section automatically uses the dark values. This is how theme systems work.Building a dark mode
CSS
:root {
--bg: #ffffff;
--bg-card: #f8fafc;
--text: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
--accent: #2563eb;
}
[data-theme="dark"] {
--bg: #0f172a;
--bg-card: #1e293b;
--text: #f1f5f9;
--text-muted: #94a3b8;
--border: #334155;
--accent: #3b82f6;
}
body {
background: var(--bg);
color: var(--text);
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
}Toggle dark mode by adding/removing
data-theme="dark" on <html>:JavaScript
document.documentElement.setAttribute('data-theme', 'dark');Every element that uses CSS variables updates instantly. No page reload needed.
Variables with calculations
Combine variables with
calc():CSS
:root {
--space-unit: 0.25rem;
}
.card {
padding: calc(var(--space-unit) * 4); /* 1rem */
margin-bottom: calc(var(--space-unit) * 6); /* 1.5rem */
}Dynamic values with JavaScript
CSS variables can be read and written from JavaScript:
JavaScript
document.documentElement.style.setProperty('--accent', '#dc2626');
const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent');This is powerful for user preferences, dynamic theming, or adjusting layouts based on runtime data.
Variables vs preprocessor variables (Sass/Less)
| Feature | CSS Variables | Sass Variables |
|---|---|---|
| Runtime | Yes — change anytime | No — compiled once |
| Cascade | Yes — can override per element | No — flat scope |
| JS access | Yes | No |
| Browser support | All modern browsers | Compiled to plain CSS |
CSS variables are strictly more powerful for anything that changes at runtime (themes, user preferences, responsive adjustments).
Naming tokens for maintainability
Treat CSS variables like a lightweight design system. Prefix by category:
--color-, --space-, --font-, --radius-. Semantic names such as --color-text-muted age better than literal ones like --gray-500 when themes change. Component-scoped variables on a .card can redefine --card-padding without polluting global scope — descendants inherit the overridden value through the cascade.Variables participate in inheritance like
color or font-size. A parent with --accent: blue passes that value down until another rule sets --accent closer to the element. This is more flexible than Sass variables, which compile away at build time and cannot respond to data-theme toggles or JavaScript updates at runtime. For spacing scales, pick a base unit and multiply: --space-4: calc(var(--space-unit) * 4).Organizing variables in larger projects
Split tokens into layers: primitive ramps (raw color steps), semantic aliases (
--color-surface maps to a primitive), and component tokens (--button-bg maps to semantic). This mirrors how design tools export styles and prevents every button from hard-wiring a single blue hex value. Review tokens in code review the same way you review API shape — renames are cheap early and expensive once dozens of components depend on them. Always provide a var() fallback when a token might be missing in partial themes or older cached CSS.Key takeaway
CSS variables eliminate repetition and enable dynamic theming. Define them on
:root for globals, scope them on elements for overrides, and use them with calc() for computed values. They're the foundation of any maintainable design system.