CSS Custom Properties

Introduction

As stylesheets grow, repeated raw values become hard to maintain: colors drift, spacing becomes inconsistent, and theme changes become expensive. CSS custom properties (often called CSS variables) solve this by turning repeated values into reusable tokens. This chapter explains practical variable patterns for scalable UI systems.

Prerequisites

  • Basic CSS syntax and cascade understanding
  • Familiarity with color, spacing, and typography styling
  • Basic component styling experience

What CSS Custom Properties Are

Custom properties are author-defined CSS values:

css
:root {
  --primary: #2563eb;
}

Use with var():

css
.button {
  background: var(--primary);
}

Code explanation:

  • --primary defines a reusable token
  • var(--primary) reads current resolved value
  • changing token in one place updates all usages

Why Use Custom Properties

Main benefits:

  • consistency across components
  • easier theme updates
  • less duplication
  • clearer design intent (semantic naming)
  • better runtime customization options

They are one of the highest-leverage tools in modern CSS architecture.

Global Tokens in :root

Common pattern:

Code explanation:

  • :root provides app-wide defaults
  • semantic names (text/primary/surface) are clearer than raw color labels
  • spacing/radius scales reduce random values in components

Using Tokens in Components

css
.card {
  background: var(--color-surface);
  color: var(--color-text);
  padding: var(--space-2);
  border-radius: var(--radius-md);
}

This keeps components aligned with shared design language.

Scope and Inheritance

Custom properties follow normal CSS cascade and inheritance rules.

css
.section {
  --color-text: #1e293b;
}
 
.section p {
  color: var(--color-text);
}

Code explanation:

  • variable defined on .section is available to descendants
  • local scope overrides :root value for that subtree

This enables component-level customization without rewriting every declaration.

Local Component Variables

Use local aliases for clarity:

css
.alert {
  --alert-bg: #fef3c7;
  --alert-text: #92400e;
 
  background: var(--alert-bg);
  color: var(--alert-text);
}

This pattern keeps component intent explicit and easier to refactor.

Fallback Values in var()

You can provide fallback when variable is missing:

css
.tag {
  background: var(--tag-bg, #e2e8f0);
}

Code explanation:

  • if --tag-bg exists, use it
  • otherwise use fallback #e2e8f0

Useful for optional theming and safer component defaults.

Variable Chaining

Variables can reference variables:

css
:root {
  --brand-primary: #2563eb;
  --button-bg: var(--brand-primary);
}

This supports token layering (brand -> component -> state).

Keep chains understandable to avoid debugging confusion.

State and Variant Theming

You can override tokens per state/variant:

This avoids duplicating full style blocks for each variant.

Dark Mode with Variables

Custom properties make dark mode much cleaner:

Code explanation:

  • only token values change in dark mode
  • component styles remain mostly unchanged

This is a major maintainability win for theme support.

Runtime Updates with JavaScript (Concept)

Variables can be changed dynamically:

js
document.documentElement.style.setProperty("--color-primary", "#7c3aed");

Useful for:

  • theme switchers
  • user personalization
  • design playgrounds

Even without JS, variables are still valuable for static maintainability.

Naming Strategy

Prefer semantic names:

  • good: --color-text, --space-2, --radius-md
  • weaker: --blue-500, --margin-big

Semantic names survive rebranding and redesign better.

Common Token Groups

Typical groups:

  • color tokens
  • spacing scale
  • typography scale
  • radius values
  • elevation/shadow levels
  • z-index layers
  • motion durations/easings

Start small and expand as your UI system grows.

Tip

Keep Token Set Intentional

Too many near-duplicate tokens create confusion. Define only values that represent real design decisions.

Practical Pattern: Tokenized Card and Button

This demonstrates cross-component consistency with minimal hardcoded values.

Common Mistakes

MistakeWhy it hurtsBetter approach
Creating tokens for every single valueToken bloat and confusionTokenize only reusable design decisions
Using non-semantic token names everywhereHard redesign/refactorUse semantic purpose-based naming
Deep variable chains with unclear ownershipDebug complexityKeep token hierarchy simple and documented
No fallback in optional contextsBroken values in reused componentsUse var(--token, fallback) where helpful
Mixing raw values and tokens randomlyInconsistent UI languagePrefer tokens for core design system values

Debugging Tips

When variable-driven styles fail:

  1. inspect computed styles in DevTools
  2. verify token exists in current scope
  3. check override order/cascade
  4. test fallback behavior with var(--token, fallback)
  5. confirm no typo in token names (--token-name must match exactly)

DevTools variable tracing is very useful for scope debugging.

Mini Exercise

  1. Define root tokens for text, primary color, spacing, and radius
  2. Refactor one card and one button to use only tokens
  3. Add one local component token override (--card-bg)
  4. Add dark mode token overrides with media query
  5. Add one fallback usage with var(--missing-token, value)

FAQ

Are CSS custom properties the same as Sass variables?

No. Sass variables are compile-time. CSS custom properties are runtime values that participate in cascade and can be changed dynamically.

Should I put all tokens in :root?

Global system tokens usually go in :root, but component-local tokens are often better scoped near the component.

Can custom properties be used in media queries directly?

Not reliably in all plain-CSS media query contexts. Many teams document breakpoint constants separately or use tooling/preprocessors.

Why does my variable work in one component but not another?

Likely scope/cascade issue. Check where the variable is defined and whether that scope includes the target element.

When should I avoid creating a token?

If a value is truly one-off and not meaningful as a design decision, a local literal may be clearer than adding token complexity.