CSS@property — typed custom properties

@property — Typed Custom Properties

Ordinary custom properties are, by design, untyped — the browser treats a value like --angle: 45deg as a raw token stream, not as an actual angle. That works fine for simple substitution, but it silently breaks two things developers reach for constantly: smooth animation of the value, and predictable fallback behavior for invalid input. The @property at-rule registers a custom property with an explicit type, a default value, and an inheritance setting, turning a plain variable into something the CSS engine actually understands.
Registering a typed custom property

CSS
@property --gradient-angle {
  syntax: '<angle>';       /* the type — an angle, not just any text */
  inherits: false;         /* does NOT inherit down to children */
  initial-value: 0deg;     /* used before it's ever set, and as a fallback */
}
  • syntax — the value type, using the same syntax component names as CSS itself: <color>, <length>, <percentage>, <number>, <angle>, <length-percentage>, or a custom set of allowed keywords like red | green | blue.

  • inheritstrue or false, controlling whether child elements pick up the value the way normal custom properties do.

  • initial-value — the value used before anything sets the property, and (critically) the value used when an invalid value is assigned.

Capability #1: the browser can now interpolate the value
An untyped custom property cannot be animated or transitioned, because the browser has no idea how to compute an "in-between" value for an arbitrary string of tokens. A typed property registered with @property tells the browser exactly what kind of value it is, so it can interpolate it the same way it interpolates a built-in property like opacity or transform.

CSS
/* WITHOUT @property — this custom property is just text.
   Animating it does nothing useful: the browser can't find a
   midpoint between the string "0deg" and the string "360deg",
   so the value jumps instantly instead of animating smoothly. */
.plain {
  --angle: 0deg;
  background: conic-gradient(from var(--angle), #0066cc, #ff6600, #0066cc);
  transition: --angle 1s linear; /* silently does nothing */
}
.plain:hover {
  --angle: 360deg;
}

CSS
/* WITH @property — the browser now knows --gradient-angle
   is an <angle>, so it can compute every intermediate value
   during the transition, producing a genuinely smooth spin. */
@property --gradient-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.spinner {
  --gradient-angle: 0deg;
  background: conic-gradient(
    from var(--gradient-angle),
    #0066cc,
    #ff6600,
    #0066cc
  );
  transition: --gradient-angle 1s linear;
}

.spinner:hover {
  --gradient-angle: 360deg; /* now animates through every angle in between */
}
This unlocks pure-CSS animations that used to require JavaScript
Animating a conic-gradient angle, a color stop, or a length driven by a custom property was previously only reliably achievable by rewriting the whole gradient on every animation frame with JavaScript. With a registered @property, the exact same effect is one transition or @keyframes rule away.
Capability #2: invalid values fall back to the default
A plain custom property has no concept of "invalid" — you can assign literally any token stream to it, and it just sits there as text until something tries to use it, at which point an incompatible value can make the whole declaration invalid and get silently dropped. A registered property validates the value against its syntax at assignment time, and substitutes the registered initial-value if the value doesn't match.

CSS
@property --card-radius {
  syntax: '<length>';
  inherits: false;
  initial-value: 8px;
}

.card {
  border-radius: var(--card-radius);
}

.card.bad {
  /* Not a valid <length> — falls back to 8px instead of
     breaking or being ignored downstream */
  --card-radius: not-a-length;
}
Registering from JavaScript
The same registration can also be done imperatively with the Properties and Values API, which is the part of CSS Houdini that @property is actually built on:

JS
CSS.registerProperty({
  name: '--gradient-angle',
  syntax: '<angle>',
  inherits: false,
  initialValue: '0deg',
})

Aspect

Plain --custom-property

@property-registered

Type checking

None — any token stream is accepted

Validated against syntax

Animatable / transitionable

No — jumps instantly

Yes — interpolates smoothly

Invalid value behavior

Kept as-is; can invalidate the rule using it

Falls back to initial-value

Inheritance

Always inherits

Explicit inherits: true/false

The property name must start with two dashes, and the rule needs all three descriptors
A @property rule missing syntax, inherits, or initial-value is treated as invalid and ignored entirely, so the plain untyped behavior applies instead. All three are required together.
Next
Query a container's computed style, not just its size: [Container Style Queries](/css/container-style-queries).