CSS custom properties (variables) are text substitutions. Write --hue: 220, and the browser stores the string "220". When you transition --hue from 220 to 340, the browser sees two strings and can't interpolate—it snaps. No console error, just a broken animation.

@property fixes that by registering a type. Declare syntax: "" and the browser knows it's a number, enabling smooth transitions.

The @property at-rule

@property --hue {
  syntax: "";
  inherits: false;
  initial-value: 220;
}

.button {
  background: hsl(var(--hue) 70% 50%);
  transition: --hue 0.3s ease;
}

.button:hover {
  --hue: 340;
}

One @property block at the top of your stylesheet. The rest of the CSS is identical to the snapping version—but now the color eases.

Supported syntax types

syntax accepts CSS value type identifiers:

  • "" — integer or float
  • ""10px, 2rem
  • ""#abc, hsl(), oklch()
  • ""45deg, 0.5turn
  • ""0% to 100%
  • "*" — any value (same as unregistered, but allows initial-value)

Animate conic gradients

Before @property, animating a conic gradient angle required JavaScript or a rotate() hack on a pseudo-element. With a registered ``:

@property --spin {
  syntax: "";
  inherits: false;
  initial-value: 0deg;
}

.loader {
  background: conic-gradient(
    from var(--spin),
    oklch(70% 0.2 220),
    oklch(60% 0.3 300)
  );
  animation: rotate 1.5s linear infinite;
}

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


The browser knows `--spin` is an ``, so `@keyframes` interpolates through the turn. The gradient rotates smoothly.

## The hidden benefit: initial-value

Unregistered custom properties that are never set return an empty string. `padding: var(--spacing-md)` becomes `padding: ""` which the parser silently discards—no error, broken layout.

Register with a valid `initial-value`:

```css
@property --spacing-md {
  syntax: "";
  inherits: true;
  initial-value: 1rem;
}

.card {
  padding: var(--spacing-md);  /* 1rem if nothing sets it */
}

This is critical for design token systems where variables flow through a large tree and you can't always guarantee the declaring element is an ancestor.

Browser support

@property shipped in Chrome 85 (late 2020), Firefox 128 (mid-2024), and Safari 16.4 (early 2023). It's Baseline 2024—cross-browser in any recent release.

Browsers that don't support @property ignore the at-rule (it's an unknown rule they skip) and fall back to normal unregistered behavior. Transitions snap instead of easing. That's progressive enhancement: modern browsers get smooth animations, older ones get the same static result. No @supports guard required unless a snap cut would break critical UI.

What just became possible

Go back through your CSS and find every place you reached for JavaScript to drive an animation that was ultimately about changing a CSS value—a color shift, a progress bar, a spinning gradient, a morphing border radius. Ask: is this actually a typed variable animation in disguise?

The custom property was always there. The type is what was missing.

What animation have you been running in JavaScript that could be a registered CSS variable and a @keyframes block? Nine times out of ten when I've asked this question, the JavaScript disappears.