Overview

CSS custom properties are runtime variables. They cascade, inherit, respond to media queries, and read from JavaScript. Use them as the design token layer for color, spacing, type, radius, and shadow. The umbrella reference is css; this page is the depth needed to retire most CSS-in-JS bookkeeping.

Define tokens on :root, read them everywhere

Put the global token set on :root so it inherits to every element. Group by purpose; prefix consistently.

:root {
  --color-ink: oklch(0.2 0 0);
  --color-surface: white;
  --color-brand: oklch(0.72 0.18 264);
 
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
 
  --radius-card: 0.75rem;
  --shadow-card: 0 1px 2px rgb(0 0 0 / 0.08);
}

A single source of truth for color and spacing makes accessibility contrast audits cheap: change the token, every consumer follows. Tailwind v4 users define the same set via @theme and get utilities for free; see tailwind.

Lean on cascade and inheritance

Custom properties cascade like every other CSS property. A child scope overrides for itself and its descendants. Use this for component variants instead of class explosions.

.card {
  background: var(--color-surface);
  color: var(--color-ink);
}
 
.card[data-variant="danger"] {
  --color-surface: oklch(0.96 0.04 25);
  --color-ink: oklch(0.32 0.18 25);
}

The variant flips two tokens once; every nested element that reads them follows. No new selectors needed, no !important, no JavaScript.

Make themes reactive with media queries

A @media block can rewrite token values at the root. Dark mode, reduced motion preferences, and print styles all become token swaps.

:root {
  --color-surface: white;
  --color-ink: oklch(0.2 0 0);
}
@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: oklch(0.18 0 0);
    --color-ink: oklch(0.96 0 0);
  }
}

For user-toggled themes, scope to a class or data- attribute on <html>: [data-theme="dark"] :root { --color-surface: ... }. Pair with a small inline script that sets the attribute before paint to avoid a flash.

Use the var(name, fallback) second argument

Every var() accepts a fallback used when the property is not defined or resolves to invalid. Use it as a contract.

.button {
  background: var(--color-accent, var(--color-brand, oklch(0.72 0.18 264)));
  border-radius: var(--radius-button, 0.5rem);
}

Fallbacks let a component ship before its host app defines the token, and let library consumers retheme without forking. They also document the expected token name at the call site.

Adopt a --namespace-purpose-modifier naming pattern

Token names outlive their first consumer. Pick one structure and apply it everywhere.

  • Namespace by purpose: --color-*, --space-*, --radius-*, --shadow-*, --font-*, --motion-*.
  • Semantic colors over palette colors at the component layer: --color-surface, --color-ink, not --color-gray-100. Build semantic tokens out of palette tokens.
  • Modifier last: --color-surface-muted, --space-section-y.

A flat --blue, --blue-2, --blue-2-light palette will collapse the moment design ships a second theme.

Retire CSS-in-JS for static styling

Custom properties replace most of what runtime CSS-in-JS was used for: dynamic theming, prop-driven variants, and inline values. Set tokens from JS by writing to style; let the cascade handle the rest.

<article className="card" style={{ "--accent": user.color } as React.CSSProperties}>
.card {
  border-block-start: 3px solid var(--accent, var(--color-brand));
}

This pattern ships zero runtime, integrates with react server components (no client boundary needed), and works in tailwind via arbitrary [--accent:...] modifiers.

Common pitfalls

  • Re-declaring a token inside a component class. The component now ignores theme switches. Set tokens at the variant scope, not the base.
  • Reading a custom property in calc() without units. calc(var(--space-2) * 2) works only if --space-2 is a length. Numbers without units fail silently.
  • Animating a custom property without @property. The browser cannot interpolate an untyped custom property. Declare with @property --foo { syntax: "<length>"; inherits: true; initial-value: 0px; } to enable transitions.
  • Token sprawl. Every team member adds a token. Audit :root quarterly; merge duplicates; delete unused ones.
  • Skipping fallbacks in library code. A consumer that has not defined --color-brand gets unset and a transparent background. Always pass a fallback in shareable code.