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-2is a length. - Token sprawl. Audit
:rootperiodically; merge duplicates and delete unused tokens. - Missing fallbacks in shareable code. A consumer without
--color-brandgets an invalid value and a transparent background.