@property Animator
Register a custom property with a type and the browser can interpolate it. Without it, custom properties are untyped strings: transitions and keyframes have no effect on them.
@property registers a custom property with a syntax type. Without it, a custom property is an opaque string: the browser stores whatever value you give it but cannot interpolate between two strings. With it, the browser knows the type, can validate values, and can animate between them.
Hover to compare
Both boxes transition their background via a custom property. Only the registered one actually transitions.
@property --color {
syntax: '<color>';
initial-value: purple;
inherits: false;
}
.box {
background: var(--color);
transition: --color 600ms ease;
}
.box:hover {
--color: red;
}
inherits: false means each element manages its own copy. Set it to true when you want child elements to pick up the parent’s value, the same way color or font-size cascade down.
Gradient rotation
A registered <angle> can drive a conic-gradient. Before @property, animating a gradient angle required JavaScript or SVG workarounds. Now it is a single keyframe.
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.ring {
background: conic-gradient(from var(--angle), purple, red, purple);
animation: spin 4s linear infinite;
}
@keyframes spin {
to { --angle: 360deg; }
}
Integer counter
A registered <integer> can drive counter-reset, which feeds content: counter() on a pseudo-element. A number counter, animated in pure CSS with no JavaScript.
@property --count {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.counter {
counter-reset: n var(--count);
animation: count-up 2s ease both;
}
.counter::after {
content: counter(n);
}
@keyframes count-up {
to { --count: 100; }
}
The same property works with animation-timeline. Swap time for scroll position and the counter advances as you scroll through a section.
Scroll-driven counter
--cpa-sn is driven by a named view-timeline on the outer wrapper. The sticky element pins to the viewport for the full scroll range, and the counter reads exactly where you are.
@property --count {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.outer {
height: 250vh;
view-timeline-name: --section;
view-timeline-axis: block;
}
.sticky {
position: sticky;
top: 0;
}
.counter {
counter-reset: n var(--count);
animation: count linear both;
animation-timeline: --section;
animation-range: contain 0% contain 100%;
}
.counter::after {
content: counter(n);
}
@keyframes count {
to { --count: 100; }
}
The same view-timeline pattern drives font-variation-settings in the Scroll-Driven Variable Fonts article, and in the companion lab demo.
Browser support
@property is supported in Chrome 85+, Firefox 128+, and Safari 16.4+ (iOS 16.4+). The first three demos work on all of those. The scroll counter uses animation-timeline, which requires Safari 18 (iOS 18, September 2024). On older iOS the counter stays at 0.