cd ../writing
// css feature · deep dive

CSS @property — the quiet revolution nobody talks about.

While the web was distracted by signals and view transitions, @property landed in every browser and quietly broke a 20-year limitation: custom CSS properties became animatable. Suddenly you can interpolate gradient angles, rotate conic sweeps, fade between colors smoothly — all without a single line of JavaScript. Here's what changed and why most people still don't realize it.

Safari 16.4+ Chrome 85+ Firefox 128+ © use freely

01The 20-year animation gap

From 2001 to 2023, CSS had a frustrating asymmetry. --my-color: red was a custom property — fine, useful. But you could not animate it. transition: --my-color 1s did nothing. The browser had no idea what type --my-color was — was it a color? a length? a string? — so it couldn't interpolate between values.

You had two terrible workarounds:

1. Animate the property the variable was used in. If background: var(--my-color), then animate background directly. Works for simple cases. Breaks when the variable is used inside a complex value like conic-gradient(from var(--angle), red, blue) — you can't animate "just the angle part" of a gradient.

2. JavaScript with requestAnimationFrame. Update the custom property 60 times per second from JS. Works, but it's wasteful, runs on the main thread, and stutters under load.

02What @property added

@property lets you declare a custom property's type. Once typed, the browser knows how to interpolate it. Animation works. Everything works.

✓ the magic three lines
@property --angle {
  syntax: '<angle>';       /* the type */
  initial-value: 0deg;     /* default if unset */
  inherits: false;         /* scope to declaring element */
}

Three lines. That's the whole feature. Once you declare it, you can animate --angle with keyframes or transitions like any built-in property.

03Animatable angles — rotating gradients

Pre-@property: to rotate a conic gradient, you had to rotate the entire element with transform: rotate. This worked for square elements but distorted pill shapes (the bounding box rotates with the gradient). With @property, you rotate only the gradient angle — the element stays put.

✓ pure gradient rotation
@property --a {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
@keyframes spin {
  to { --a: 360deg; }
}
.bloom {
  background: conic-gradient(from var(--a), red, blue, red);
  animation: spin 2s linear infinite;
}
// live demo · gradient angle interpolation

04Color interpolation

Before @property, you could animate background-color from red to blue with transition. But you couldn't animate a custom property like --brand-color, then use it in multiple places, then update all of them with one animation. Now you can.

✓ shared color animation
@property --c {
  syntax: '<color>';
  initial-value: rgb(139, 92, 246);
  inherits: false;
}
@keyframes color-cycle {
  0%, 100% { --c: rgb(139, 92, 246); }
  50%      { --c: rgb(255, 64, 129); }
}
.card {
  background: var(--c);
  border-color: var(--c);
  box-shadow: 0 0 20px var(--c);
  animation: color-cycle 2s infinite;
}
// live demo · color interpolation

05Animatable numbers — counting up

With syntax: '<number>', you can animate raw numeric values. Combined with CSS counters, this enables animated number counters with zero JavaScript — the kind every stats page needs.

✓ pure-CSS counter
@property --n {
  syntax: '<number>';
  initial-value: 0;
  inherits: false;
}
@keyframes count {
  to { --n: 100; }
}
.counter::before {
  counter-reset: c var(--n);
  content: counter(c);
  animation: count 2s ease-out forwards;
}
// live demo · animated counter

06The complete syntax reference

Every type @property supports. Pick the most specific one for your use case — looser types still work but you lose some interpolation behavior.

✓ syntax reference
'<color>'           /* rgb, hsl, hex, named colors */
'<length>'          /* 10px, 1rem, 100vh */
'<percentage>'      /* 25% */
'<length-percentage>' /* either of the above */
'<number>'          /* unitless: 1.5, -3 */
'<integer>'         /* whole numbers only */
'<angle>'           /* 90deg, 1turn, 1.5rad */
'<time>'            /* 200ms, 1s */
'<resolution>'      /* 2dppx, 96dpi */
'<image>'           /* url(...), linear-gradient(...) */
'<url>'             /* url(...) only */
'<custom-ident>'    /* identifier — not interpolatable */
'*'                 /* any value — not interpolatable */

/* Combine with + (one or more, space-separated)
   or # (one or more, comma-separated)
   or | (either of these types) */
'<length>+'         /* one or more lengths */
'<color>#'          /* comma-separated colors */
'<length> | <percentage>'  /* either type */

07Gotchas to know

Inheritance is opt-in. The default is inherits: false. If you want the property to propagate down the tree (like normal custom properties do), set inherits: true. Most of the time you want false — it scopes the property to the element that uses it.

You can't redefine in JS. CSS.registerProperty from JavaScript exists but most browsers prefer the CSS form. Stick with @property in CSS.

Older browsers ignore it silently. If iOS 16.3 or earlier sees @property, it ignores the declaration and treats the property as untyped — your animation won't fail loudly, it just won't animate. Use @supports (background: paint(x)) if you need to feature-detect, but ~95% of users globally are on supported versions now.

Don't over-use it. Every @property declaration adds a tiny amount of style computation work. If you only need to animate one value once, just animate the underlying CSS property directly. Save @property for cases where the animatable value is buried inside a complex value (gradients, transforms, filters).

What it really unlocked

The headline is "animatable custom properties," but the real impact is bigger: @property moved animations that used to live in JavaScript back into the GPU-accelerated CSS pipeline. The Gemini "thinking" effect, the holographic foil on Pokemon cards, the gradient borders on Linear's buttons, the AI shimmer on Stripe's beta features — none of these need JS anymore.

That's a real shift in capability. CSS doesn't get many of those.