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.
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.
@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.
@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;
}
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.
@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;
}
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.
@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;
}
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.
'<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.
@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.