Loading...

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:

Global design tokens on :root

css
: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:

Primary button wired to tokens

css
.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:

Fallback when a token is absent

css
.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 subtree with its own accent token

css
.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:

Light default, dark palette on data-theme

css
: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

  1. Add five tokens on :root (brand, surface, text, radius, spacing); style a button with only var().
  2. On .card, set --accent and use it for border and heading color; add .is-featured with a new --accent.
  3. Remove one token temporarily and rely on var(--name, fallback) so layout still works.
  4. Add [data-theme="dark"] token overrides; toggle the attribute in DevTools and watch body colors change.
  5. Find one repeated #2563eb in old CSS; replace reads with var(--brand) and define --brand once.

Common mistakes

  • Writing brand: blue instead 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 --names with two leading dashes; read with var(--name) or var(--name, fallback)
  • :root holds 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

Ready to Start

Test Your Knowledge

Challenge yourself with this interactive quiz and see how well you understand the topic

❓
6
Questions
🎯
70%
To Pass
♾️
∞
Time
🔄
∞
Attempts

📝 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