Episode 1 — Fundamentals / 1.6 — CSS Core Fundamentals
1.6.g — CSS Variables (Custom Properties)
In one sentence: CSS custom properties (
--name) let you define reusable, inheritable, runtime-updatable values — they are the foundation of theming, design tokens, and dynamic styles without preprocessors.
Navigation: ← 1.6.f — Modern CSS Functions · 1.6.h — Design Systems →
1. Syntax
Defining a variable
:root {
--color-primary: #2563eb;
--spacing-md: 1rem;
--font-body: system-ui, sans-serif;
}
--prefix is required (double hyphen).- Defined on any selector —
:rootmakes them globally available via inheritance. - Values are inherited down the DOM tree like
colororfont-family.
Using a variable
.button {
background-color: var(--color-primary);
padding: var(--spacing-md);
font-family: var(--font-body);
}
Fallback values
.card {
color: var(--color-text, #333);
/* If --color-text is not defined, use #333 */
}
Fallbacks can be nested: var(--a, var(--b, red)).
2. Why :root?
:root targets the <html> element with a specificity of a pseudo-class (0,0,1,0). Defining variables there makes them available to every element via inheritance.
:root {
--radius: 0.5rem;
}
/* Every element can use --radius */
.card { border-radius: var(--radius); }
.button { border-radius: var(--radius); }
.input { border-radius: var(--radius); }
3. Scoping — local overrides
Custom properties follow inheritance, so you can override them at any level:
:root {
--color-primary: #2563eb; /* blue */
}
.dark-theme {
--color-primary: #60a5fa; /* lighter blue for dark backgrounds */
}
.danger-zone {
--color-primary: #dc2626; /* red in this section */
}
Any descendant of .dark-theme that uses var(--color-primary) gets the lighter blue — no extra selectors needed.
4. Theming with custom properties
Light / dark mode
:root {
--bg: #ffffff;
--text: #1a1a2e;
--surface: #f1f5f9;
}
[data-theme="dark"] {
--bg: #0f172a;
--text: #e2e8f0;
--surface: #1e293b;
}
body {
background-color: var(--bg);
color: var(--text);
}
Switch themes by toggling a data-theme attribute — all styles update automatically through variable inheritance.
Respecting user preference
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #e2e8f0;
}
}
5. Dynamic updates with JavaScript
Custom properties are live — changing them at runtime updates all dependent styles immediately:
document.documentElement.style.setProperty('--color-primary', '#10b981');
This is far more efficient than toggling classes for every affected element — the browser recomputes styles from the single variable change.
6. Custom properties vs preprocessor variables
| CSS Custom Properties | Sass/Less Variables | |
|---|---|---|
| Runtime | Live — computed at render time | Compiled away at build time |
| Inheritance | Yes — flows down the DOM | No — flat scope |
| Theming | Yes — override per context | Requires recompilation |
| JS access | Yes — getComputedStyle / setProperty | No — must rebuild CSS |
| Fallbacks | Built-in var(--x, fallback) | Preprocessor conditionals |
CSS custom properties and preprocessor variables are complementary — use preprocessor variables for build-time constants (file paths, mixins) and custom properties for runtime-dynamic values (themes, user preferences).
7. Patterns and tips
Spacing scale
:root {
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 2rem;
--space-xl: 4rem;
}
Component-scoped variables
.card {
--card-padding: 1.5rem;
--card-radius: 0.75rem;
padding: var(--card-padding);
border-radius: var(--card-radius);
}
.card.compact {
--card-padding: 0.75rem;
--card-radius: 0.5rem;
}
The component owns its knobs — consumers override the variables, not the internals.
Computed variables with calc()
:root {
--base-size: 1rem;
--scale: 1.25;
}
h3 { font-size: calc(var(--base-size) * var(--scale)); }
h2 { font-size: calc(var(--base-size) * var(--scale) * var(--scale)); }
h1 { font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale)); }
8. Key takeaways
--custom-propertydefined,var(--custom-property)consumed.- Defined on
:root= globally available; scoped to any selector for local overrides. - Custom properties inherit — override in a parent, all descendants update.
- Theming (dark mode, brand variants) becomes trivial with variable overrides.
- JS can update custom properties at runtime — no rebuild needed.
- Use
var(--x, fallback)for defensive coding.
Explain-It Challenge
Explain without notes:
- Why defining variables on
:rootmakes them globally available. - How to implement dark mode using only CSS custom properties and a
data-themeattribute. - One advantage CSS custom properties have over Sass variables at runtime.
Navigation: ← 1.6.f — Modern CSS Functions · 1.6.h — Design Systems →