Overview

Custom properties are runtime variables: they cascade, inherit, respond to media queries, and read from JavaScript. Use them for color, spacing, type, radius, and shadow tokens. The umbrella page is css.

Define tokens on :root, read them with var()

Group by purpose and prefix consistently.

:root {
  --color-ink: oklch(0.2 0 0);
  --color-surface: white;
  --space-2: 0.5rem;
  --radius-card: 0.75rem;
}

A single source of truth makes contrast audits cheap: change the token and every consumer follows (accessibility). Tailwind v4 users declare the same set in @theme and get utilities too; see tailwind-theme.

Override by scope for variants

A child scope overrides a token for itself and its descendants, replacing class explosions and !important.

.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);
}

Swap themes with media queries and attributes

Rewrite token values under @media (prefers-color-scheme: dark) for system-driven themes. For a user toggle, scope to an attribute on the root element and set it before paint.

:root { --color-surface: white; }
@media (prefers-color-scheme: dark) { :root { --color-surface: oklch(0.18 0 0); } }
:root[data-theme="dark"] { --color-surface: oklch(0.18 0 0); }

Do not write [data-theme="dark"] :root; that selector matches nothing when the attribute is on <html>. See tailwind-dark-mode for the toggle script.

Give var() a fallback in shared code

var(--name, fallback) applies the fallback when the property is undefined or invalid. Library and shared components should always pass one so they work before the host defines the token.

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

Name tokens --namespace-purpose-modifier

Use namespaces (--color-*, --space-*, --radius-*, --shadow-*, --font-*) and semantic names at the component layer (--color-surface, not --color-gray-100); build semantic tokens from palette tokens. A flat --blue-2-light palette collapses when a second theme arrives.

Set values from JavaScript instead of CSS-in-JS

Write a token on an element’s style and let the cascade handle the rest. This ships no runtime and needs no client boundary in react server components.

<article className="card" style={{ "--accent": user.color } as React.CSSProperties} />

Register typed properties with @property

An untyped custom property cannot be interpolated, so transitions on it jump. Register it to animate it.

@property --tilt {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property is Baseline newly available (since July 2024). See css-animations.

Avoid common pitfalls

  • Redeclaring a token in a component’s base class. The component ignores theme switches; set tokens at the variant scope.
  • Unitless values in calc(). calc(var(--space-2) * 2) works only if --space-2 is a length.
  • Token sprawl. Audit :root periodically; merge duplicates and delete unused tokens.
  • Missing fallbacks in shareable code. A consumer without --color-brand gets an invalid value and a transparent background.