CSS variables.
Define once, use everywhere.
CSS variables — officially called custom properties — let you define a value once and reference it anywhere. Change the definition and every usage updates. They're part of the CSS spec, not a preprocessor: they cascade, they can be changed at runtime with JavaScript, and they enable theming, dark mode, and component-level customization without duplicating CSS. This page covers syntax, scope, fallbacks, and the patterns that make them useful.
Short answer: Define variables with two dashes on :root for global values: :root {'{ --primary: #2965f1; }'}. Use them with var(): color: var(--primary);. Add a fallback as the second argument: var(--primary, blue). Change the definition to update every usage. Override on a component or theme to scope the change. Use JavaScript's style.setProperty('--primary', 'red') for runtime changes like dark mode toggles.
✅ Define and use
The basic pattern.
:root {
--primary: #2965f1;
--text: #1f2937;
--bg: #ffffff;
--spacing: 1rem;
--radius: 8px;
}
.button {
background: var(--primary);
color: var(--bg);
padding: var(--spacing);
border-radius: var(--radius);
}
.card {
background: var(--bg);
color: var(--text);
padding: var(--spacing);
border-radius: var(--radius);
border: 1px solid #e5e7eb;
}
Change the variable, change every usage:
:root {
--primary: #2965f1; /* blue */
}
Change to:
:root {
--primary: #e34c26; /* orange */
}
Every .button background updates instantly.
No search-and-replace, no class changes,
no recompilation.
✅ Same values, one source of truth.
✅ Consistent spacing across components.
✅ Rebrand by changing 5 lines.
✅ Dark mode by overriding 5 variables.
✅ Scoping and cascade
Variables cascade — override them anywhere.
:root {
--primary: #2965f1;
}
.button {
background: var(--primary);
color: white;
padding: 0.5rem 1rem;
}
/* Scope an override to a parent /
.dark-section {
--primary: #8b5cf6; / purple inside this section */
background: #111827;
padding: 2rem;
}
/* Set a default for a component */
.card {
--card-padding: 1rem;
padding: var(--card-padding);
}
/* Override the component's own variable */
.card--spacious {
--card-padding: 2rem;
}
HTML:→ blue→ purple✅ Same .button class. ✅ Different color because --primary is overridden at the parent. ✅ Cascade applies — children inherit the variable from their ancestors. Component-level: .card → padding: 1rem .card--spacious → padding: 2rem Same .card base, modifier overrides one variable. This is the pattern for variants.
✅ Dark mode with variables
One override, every component updates.
:root {
--bg: #ffffff;
--text: #1f2937;
--border: #e5e7eb;
--primary: #2965f1;
}
[data-theme="dark"] {
--bg: #0f172a;
--text: #f1f5f9;
--border: #334155;
--primary: #60a5fa;
}
body {
background: var(--bg);
color: var(--text);
}
.card {
background: var(--bg);
border: 1px solid var(--border);
padding: 1rem;
border-radius: 8px;
}
.button {
background: var(--primary);
color: white;
}
/* JS toggles the theme: /
/ document.documentElement.dataset.theme = 'dark' */
Light mode:
body { background: #ffffff; color: #1f2937; }
.card { background: #ffffff; border: #e5e7eb; }
.button { background: #2965f1; }
Dark mode (JS adds data-theme="dark"):
body { background: #0f172a; color: #f1f5f9; }
.card { background: #0f172a; border: #334155; }
.button { background: #60a5fa; }
✅ Every component updates automatically.
✅ No duplicated CSS — one override block.
✅ No class toggling on every element.
✅ Persists via localStorage + first-paint
inline script.
This is how dark mode is done in 2026.
One attribute, one CSS block. Everything else
uses var().
Alternative: @media (prefers-color-scheme: dark)
for system-preference-based theming.
Don't use var() without a fallback in third-party components. If your component uses var(--primary) and the page doesn't define --primary, the color falls back to initial — which is usually transparent or black, not what you want. Always include a fallback for third-party or reusable components: color: var(--primary, #2965f1). For internal components on your own site, the fallback is optional — you control the CSS and the variable. But for anything reusable or embeddable, the fallback is the difference between "works everywhere" and "breaks when embedded."
⚠️ Common mistakes
The CSS-variable mistakes that cost real time.
color: --primary without var()Doesn't work — var() is required. Fix: var(--primary).
--color vs --Color — different variables. Fix: consistent casing.
Defined on .header, used on .footer. Fix: define on :root or a common ancestor.
Breaks when embedded on a page without the variable. Fix: var(--primary, blue).
var(--display) can't change display behavior — it's a value, not a property. Fix: use the variable's value in a valid property.
Light mode breaks if dark mode vars are the only ones. Fix: define defaults on :root, override in dark mode.
Two components both define --primary at different scopes. Fix: use component-prefixed names: --card-primary.
They work differently. Fix: understand CSS variables cascade at runtime; Sass vars are compile-time.
❓ Frequently asked
What are CSS variables?
CSS variables — officially custom properties — are values you define once and reuse. They start with two dashes: --primary-color: #2965f1. You use them with var(): color: var(--primary-color). Unlike preprocessor variables, CSS variables are part of the CSS spec — they cascade, they can be changed at runtime, and they work in DevTools. Define them on :root to make them available everywhere.
What is the difference between CSS variables and Sass variables?
Sass variables (`$var) are compile-time — they're replaced with their values before the CSS is generated. Once compiled, they don't exist. CSS variables (--var) are runtime — they exist in the browser and can be changed via JavaScript, media queries, or the cascade. This makes CSS variables far more powerful for theming and dynamic styles.
Where should I define CSS variables?
On :root for global values — the document root element, available to every element. For component-scoped values, define them on the component's own selector: .card { --card-padding: 1rem; }. The cascade then applies — an element inherits variables from its ancestors. For theming, define default values on :root and override them in [data-theme='dark'] { ... }.
How do I use var() with a fallback?
The var() function takes a second argument — the fallback value, used if the variable is not defined. Syntax: color: var(--primary, blue). If --primary is defined, use it; otherwise use blue. You can chain fallbacks: var(--primary, var(--brand, blue)). This is invaluable for third-party components.
Can I change a CSS variable with JavaScript?
Yes — this is one of the biggest advantages over Sass. Use document.documentElement.style.setProperty('--primary', 'red') to change a variable on the root. Use element.style.setProperty('--local-var', 'value') to change it on a specific element. Every element using var(--primary) updates instantly. This is how dark mode toggles work without duplicating CSS.
Why doesn't my CSS variable work?
Four common causes. (1) Typo in the name — variables are case-sensitive. (2) The variable is out of scope — you defined it on .header but are using it on .footer. (3) Missing var() — you wrote color: --primary instead of color: var(--primary). (4) The value has invalid characters. Also check DevTools — the computed value shows whether the variable resolved.