ESC

Type to search the knowledge base.

CSS Custom Properties

CSS variables that cascade and inherit — define tokens, theme at runtime, fall back safely, and avoid the invalid-at-computed-value trap.

beginner3 min read
  • css
  • custom-properties
  • variables

Custom properties (--name) are CSS values you define and reuse with var(). Unlike preprocessor variables, they are live in the browser: they inherit, cascade, and can change at runtime with a class, media query, or one line of JavaScript.

Docs: MDN using CSS custom properties, var().

Define and consume

:root {
  --brand: #0b6bcb;
  --radius: 0.5rem;
  --space-2: 0.5rem;
  --font-sans: system-ui, sans-serif;
}

.button {
  background: var(--brand);
  border-radius: var(--radius);
  padding: var(--space-2) calc(var(--space-2) * 2);
  font-family: var(--font-sans);
}

Names are case-sensitive. By convention tokens live on :root or a theme scope; component-local vars live on the component root.

Fallbacks

color: var(--text, #14212b);
background: var(--card-bg, var(--surface, white));

The fallback is used when the property is missing or invalid at computed-value time on that element — not as a browser-support polyfill for var() itself.

Inheritance and scope

.card {
  --pad: 1rem;
  padding: var(--pad);
}

.card--compact {
  --pad: 0.5rem; /* children using var(--pad) update too */
}
<article class="card card--compact">
  <p style="margin: 0; padding: var(--pad)">Inherits --pad</p>
</article>

This is the theming superpower: set --brand on .theme-dark or [data-theme="dark"] and the tree retints.

Runtime theming

document.documentElement.style.setProperty("--brand", "#c45c26");
[data-theme="dark"] {
  --surface: #0f1419;
  --text: #e7ecf1;
  --brand: #6cb6ff;
}

No rebuild required for simple theme switches. Prefer class/data-* on html over sprinkling inline styles.

Invalid at computed-value time

:root {
  --gap: 8;
}

.row {
  /* Wrong: unitless 8 is invalid for gap → property becomes invalid */
  gap: var(--gap);
}

.row-fixed {
  gap: calc(var(--gap) * 1px); /* OK if --gap is number */
}

If var() resolves to something illegal for that property, the declaration is invalid and the property falls back to inherited/initial — not necessarily to a previous line in the same rule. Store full values (8px) or always calc with units.

What custom props are bad at

  • Media query break points as var(--bp) in @media (min-width: var(--bp)) — generally not allowed (MQ syntax needs certain tokens).
  • Replacing entire selector logic — vars hold values, not selectors.
  • Huge binary data in vars — keep tokens small.

Interview out-loud

“Custom properties are runtime CSS variables defined with --name and read with var(). They inherit and cascade, so theming is overriding tokens on a parent. I put design tokens on :root, component tokens on the component root, always include units or calc carefully, and use fallbacks as safety defaults. They differ from Sass variables because the browser computes them per element.”

Footguns

  1. Unitless numbers in length contexts.
  2. Expecting var in every at-rule syntax position.
  3. Circular references (--a: var(--b); --b: var(--a)).
  4. Setting hundreds of vars per frame in JS — style recalc cost.
  5. Using props for one-off magic numbers with no naming discipline.

Component API with vars

Expose a small public token surface on the host element; keep private vars underscored.

.tabs {
  --tabs-gap: 0.5rem;
  --tabs-indicator: var(--brand);
  display: flex;
  gap: var(--tabs-gap);
}

.tabs__tab[aria-selected="true"] {
  box-shadow: inset 0 -2px 0 var(--tabs-indicator);
}

/* Consumer override */
.tabs.tabs--compact {
  --tabs-gap: 0.25rem;
}

This is how design systems let product teams theme without forking component CSS. Register required tokens in Storybook/docs. Avoid 40 public vars on one component — three to eight is usually enough.

Debugging

In DevTools, select an element → Computed → show custom properties (or the Styles panel lists inherited vars). If a var() is invalid, the property is crossed out and you will not see a “fallback cascade” like Sass; fix the resolved value or supply var(--x, fallback).

Further reading

Related guides