CSS Variables
Filters polished pixels. Custom properties (CSS variables) polish maintenance: one --brand token on :root, dozens of buttons read var(--brand) — change the token once, the whole UI follows. They cascade and inherit like ordinary properties, but you define the names yourself.
Names you own: --token
A custom property looks like a normal declaration, but the name must start with two dashes:
:root {
--brand: #2563eb;
--brand-contrast: #ffffff;
--surface: #f8fafc;
--text: #0f172a;
--radius-md: 0.5rem;
--space-3: 0.75rem;
}
:root matches the document root — a common home for site-wide tokens (colors, spacing, radii). You can also define --names on any selector; descendants inherit the computed value unless overridden closer.
Reading values with var()
Use var(--name) anywhere a property value is allowed:
.btn-primary {
background: var(--brand);
color: var(--brand-contrast);
border: none;
border-radius: var(--radius-md);
padding: var(--space-3) calc(var(--space-3) * 1.5);
font: inherit;
cursor: pointer;
}
.btn-primary:hover {
background: color-mix(in srgb, var(--brand) 88%, black);
}
color-mix() is optional polish — the lesson focus is tokens + var(). Keep token names consistent (--space-3, not --spacing3 in one file and --space_3 in another).
Fallbacks inside var()
If a token is missing or invalid at computed-value time, the declaration can fail. Pass a second argument as backup:
.card {
background: var(--surface, #ffffff);
color: var(--text, #111827);
border-radius: var(--radius-md, 8px);
box-shadow: var(--shadow-sm, 0 1px 2px rgba(15, 23, 42, 0.08));
}
Fallbacks help during refactors and in partial themes. They do not fix typos in the name you pass to var() — --surfce is still wrong even with a fallback.
Scope and local overrides
Tokens inherit down the tree. A component can redefine a token for its subtree only — handy for emphasis cards or nested widgets.
📊 Where to put custom properties
| Scope | Typical selector | Use when |
|---|---|---|
| Global tokens | :root |
Brand colors, spacing scale, shared radii |
| Component theme | .card, .alert |
Defaults for one pattern; children use var(--…) |
| State / variant | .card.is-warning, [data-tone="muted"] |
Swap a few tokens without copying every longhand rule |
| Runtime swap | [data-theme="dark"], .theme-dark on <html> |
Palette flip; rules keep reading the same var(--surface) names |
Override near the element that needs the new values. Distant :root edits affect everything — sometimes that is what you want; sometimes a local --accent on .pricing-card is cleaner.
Local accent on a card
Same structural CSS, different accent token:
.card {
--accent: var(--brand);
background: var(--surface);
border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
border-radius: var(--radius-md);
padding: var(--space-3);
}
.card.is-featured {
--accent: #7c3aed;
}
.card__title {
color: var(--accent);
}
.card.is-featured only changes --accent; title and border pick it up through var(). That is the maintenance win filters cannot give you — not a new blur, but one knob for a family of rules.
Theme swap without duplicating every rule
Toggle a attribute or class on the root; redefine tokens once:
:root {
--surface: #f8fafc;
--text: #0f172a;
--brand: #2563eb;
}
[data-theme="dark"] {
--surface: #0f172a;
--text: #e2e8f0;
--brand: #60a5fa;
}
body {
background: var(--surface);
color: var(--text);
}
JavaScript can set document.documentElement.dataset.theme = 'dark' — variables update live without recompiling CSS. Preprocessor variables cannot do that in the browser.
Custom properties vs Sass $variables
Sass $vars die at build time. --tokens exist in the cascade in the browser — inherit, override, and respond to media queries on the selector where they are defined (you still cannot put var() in a property name). Many teams use both: Sass for mixins, native tokens for theming.
Try it yourself
- Add five tokens on
:root(brand, surface, text, radius, spacing); style a button with onlyvar(). - On
.card, set--accentand use it for border and heading color; add.is-featuredwith a new--accent. - Remove one token temporarily and rely on
var(--name, fallback)so layout still works. - Add
[data-theme="dark"]token overrides; toggle the attribute in DevTools and watchbodycolors change. - Find one repeated
#2563ebin old CSS; replace reads withvar(--brand)and define--brandonce.
Common mistakes
- Writing
brand: blueinstead of--brand: blue - Using
var(--token)as a property name (invalid — only values) - Typo in token name with no fallback — whole declaration drops
- Defining tokens only inside a tiny selector then wondering why
:root-level components cannot see them (inheritance still applies — check selector reach) - Expecting custom properties to replace learning cascade/specificity — they use the cascade
Summary
- Define
--nameswith two leading dashes; read withvar(--name)orvar(--name, fallback) :rootholds global tokens; components can override locally for subtrees- Tokens inherit and cascade — ideal for palettes and spacing scales
- Theme toggles redefine tokens, not every color rule
- Modern Toolkit continues with calc and math functions next
🧠 Test Your Knowledge
Test Your Knowledge
Challenge yourself with this interactive quiz and see how well you understand the topic
📝 Instructions
- Read each question carefully
- Select the best answer for each question
- You can retake the quiz as many times as you want
- Your progress will be shown at the top