Quick answer: A CSS custom property is a variable you define in CSS, written with two dashes, such as --brand: teal, and read with var(--brand). They cascade and inherit like any other property, so you can theme a section by redefining one value. Add @property to give a custom property a type and an initial value, which makes it animatable.The sections below cover CSS custom properties from declaration to @property: how they cascade, how fallbacks work, theming a section by redefining one variable, and where CSS custom properties still cannot help.

Declaring CSS custom properties

:root {
  --brand: #0f766e;
  --space: 1rem;
  --radius: 0.5rem;
}

.button {
  background: var(--brand);
  padding: var(--space);
  border-radius: var(--radius);
}

:root is the document element, so variables declared there are available everywhere. The value is a live runtime string: you can change --brand at any selector and every descendant that reads it updates, with no recompilation.

Fallbacks and defaults

var() accepts a fallback after a comma, used when the property is not defined or is an invalid value for the context.

.card {
  color: var(--text, #1e293b);           /* fallback to near-black */
  padding: var(--pad, var(--space, 1rem)); /* nested fallbacks */
}

A pattern worth learning: a component sets a default at its root, and a parent overrides it to customise just that instance. It is how component libraries expose theming without a separate API.

.badge { --badge-bg: #ccfbf1; --badge-fg: #0f766e;
         background: var(--badge-bg); color: var(--badge-fg); }

.badge.danger { --badge-bg: #fee2e2; --badge-fg: #991b1b; }

Theming a whole section with CSS custom properties

Because custom properties inherit, redefining them on a container themes everything inside without touching the components.

.promo { --brand: #b45309; }   /* everything in .promo uses amber now */

@media (prefers-color-scheme: dark) {
  :root { --brand: #5eead4; }
}

This is the cleanest way to support dark mode: keep the same rules and swap the variables. No duplicated stylesheets.

Typed custom properties with @property

By default a custom property is an untyped string, which means the browser cannot animate it or give it a starting value. @property registers a type, an initial value and whether it inherits.

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

.spinner {
  background: conic-gradient(from var(--angle), teal, transparent);
  animation: spin 2s linear infinite;
}

@keyframes spin { to { --angle: 360deg; } }

Without @property that animation would jump instead of interpolating, because the browser would not know the value is an angle. With it, custom properties become first-class animatable values. The syntax string accepts types such as <color>, <length>, <percentage> and <angle>.

Two cautions

  • They are not preprocessor variables. A Sass variable is replaced at build time; a custom property exists at runtime, cascades and can be changed by JavaScript with element.style.setProperty('--x', value).
  • Do not over-abstract. A handful of tokens — colour, spacing, radius, font size — covers most projects. Hundreds of nested variables make a stylesheet harder to read than the values they replace.

Next: CSS nesting and cascade layers. To learn modern CSS with a mentor, Ampersand Academy teaches web development one-to-one.

Frequently asked questions

What is a CSS custom property?

A variable written with a double dash, such as –brand, and read with var(–brand). It cascades and inherits at runtime, unlike a preprocessor variable that is replaced at build time.

Do CSS variables work in all browsers?

Yes. Custom properties and the var function are widely supported in every current browser, so they are safe to use in production.

How do I animate a CSS variable?

Register it with @property, giving it a syntax such as angle, colour or length and an initial value. Then the browser can interpolate it and you can animate it in keyframes.

What is the difference between –x and var(–x, fallback)?

–x declares the variable. var(–x, fallback) reads it and uses the fallback value when the variable is undefined or invalid in that context.

How do I support dark mode with custom properties?

Declare your colour tokens at :root, then redefine them inside a prefers-color-scheme dark media query. The same rules apply and every component picks up the new values.

Last updated on · Written by