CSS Custom Properties Table

PieceWhat it doesField note
--brand: #09fDeclares a custom propertyNames start with two dashes - values are raw tokens, not computed CSS
var(--brand)Reads the propertyInvalid without a definition - the value becomes guaranteed-invalid
var(--brand, #333)Read with fallbackFallback applies only when --brand is undefined, not when invalid
:root { --gap: 8px }Global scope declarationCustom properties inherit - :root is the whole document
@property --angleTyped, animatable propertyGives a syntax type and initial value; enables transitioning
calc(var(--gap) * 2)Computed from variablesThe token is substituted before calc evaluates
element.style.setPropertyJS writes the variableThe JS-CSS bridge - theme switching without class juggling
Reference: the MDN custom properties guide. The mental model that prevents confusion: custom properties are NOT Sass variables - they are real CSS values that inherit, cascade, and resolve at runtime, which is why theme switching works by changing one property on :root and every var() reader updates. Bottom line: define design tokens on :root, read them everywhere with var(), add @property when a value needs a type or animation, and give every var() a sensible fallback for the one file that forgot to declare it. Related tools: CSS animations table (the @property animation unlock), box model table, and selectors table for scoping declarations.

Custom properties - the CSS variables that start with two dashes - are real CSS values: they inherit, cascade, and resolve at runtime. That runtime nature is exactly what separates them from preprocessor variables, and exactly why theme switching works by changing a single property on :root. The table below is the working seven: declaration, reading with fallbacks, scoping, the @property typing upgrade, and the JavaScript bridge.

Bottom line: define design tokens once on :root, read them everywhere with var(), and every consumer updates the moment the token changes - colors, spacing scales, dark themes. The fallback form var(--brand, #333) is insurance against the one file that forgot to declare it; it does not protect against an INVALID value, which is a different failure the table's notes explain.

The honest part: the Sass-variables confusion wastes the feature. Sass variables are compile-time - baked in, invisible to the browser, unchangeable after load. Custom properties live in the DOM: JavaScript reads and writes them via setProperty, media queries flip them, and with @property they can even animate. If the value should ever change after the page loads, it wants to be a custom property.

How to use

  1. Declare tokens on :root with double-dash names - the two dashes are the entire naming syntax.
  2. Read with var() everywhere, adding a fallback only where a missing token would break the design.
  3. Upgrade to @property when you need a typed initial value or want to animate a property that values cannot normally animate.

Frequently asked questions

What is the difference between CSS custom properties and Sass variables?

Sass variables exist at build time: the compiler substitutes them and the browser never sees them - they cannot change after load. Custom properties are live CSS values: they cascade, inherit, respond to media queries, and can be rewritten by JavaScript at runtime. Dark themes are the clearest demonstration - flipping one --background custom property in a media query recolors every var() consumer instantly, something a compiled variable structurally cannot do.

When does the var() fallback actually apply?

Only when the referenced property is UNDEFINED - var(--brand, #333) falls back if nothing declared --brand. If --brand IS defined but its value is invalid for the property using it, the fallback is ignored and the property becomes its inherited or initial value instead. The distinction matters for theming: a typo in the token value is a different failure from a missing token, and DevTools shows which one happened in the computed styles.

What does @property add to custom properties?

A type and a starting point. Custom properties are raw tokens by default - the browser cannot interpolate between two unknown strings, which is why animating a var()-based color or angle just snaps. @property --angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; } declares the type, enabling real transitions and animations on the value. It is the bridge between token-driven design and animation-driven interfaces.

How do I change a custom property from JavaScript?

element.style.setProperty('--brand', newValue) writes it on that element, and every var() reader inside inherits the change - the standard theme-switch mechanism, no class juggling required. Reading back uses getComputedStyle(element).getPropertyValue('--brand'), which resolves the cascade and returns the effective value. Because properties inherit, writing on document.documentElement :root restyles the entire page - one call, whole-site theme.

Related tools