Animating Custom Properties with @property: Typed Variables That Interpolate
You set a transition on a custom property, change its value on hover, and nothing animates — the value just snaps. This is the single most common surprise with CSS variables, and the fix is not a different transition but a one-time registration. This guide, part of the CSS Custom Properties Architecture section, explains how the @property at-rule gives a custom property a type so the browser knows how to interpolate it, turning previously un-animatable values — gradient angles, raw numbers, colors fed into calc() — into smoothly transitionable motion with no JavaScript.
The narrow scenario: you want a conic gradient to rotate, a counter-like number to count up, or a hue to glide across hover states, driven entirely by a custom property and a transition.
Why registration is required, and when it is not worth it
By default every custom property is, for animation purposes, of the universal type <custom-ident> — an opaque token the browser cannot subdivide. Because there is no halfway point between two arbitrary strings, the engine treats the change as discrete: it holds the old value, then jumps to the new one at the end of the transition. That is why transition: --angle 1s does nothing visible even though the syntax is accepted.
@property removes the ambiguity. Declaring syntax: "<angle>" tells the browser the value is an angle, which it knows how to interpolate degree by degree, so the same transition now animates frame by frame on the compositor or main thread as appropriate. The cost is one at-rule per animatable property and the discipline of supplying a valid initial-value. It is worth it whenever the visual change is a continuous quantity — angle, length, number, percentage, or color. It is not worth it for values that are inherently discrete (a display keyword, a grid-template string); those belong to other techniques such as transitioning display with allow-discrete. There is no accessibility downside to registration itself, but the motion it unlocks must still collapse under reduced motion preferences.
Registered versus unregistered: animatable versus not
The diagram contrasts the two paths. An unregistered property is typeless, so a transition produces a discrete jump. A registered property carries a typed syntax, so the same transition interpolates.
A complete working implementation: a rotating conic gradient ring
The classic demonstration is a conic-gradient border whose angle animates — impossible to animate before @property because gradient angles live inside a string the browser could not interpolate. Here a single registered --angle drives the rotation on hover.
<button class="ring" type="button">Hover for sweep</button>
/* Register the property so the angle becomes a typed, animatable value.
All three descriptors are mandatory for a valid @property rule. */
@property --angle {
syntax: "<angle>"; /* tells the engine to interpolate as an angle */
inherits: false; /* stays local to the element it is set on */
initial-value: 0deg; /* a guaranteed-valid starting value */
}
.ring {
--angle: 0deg;
position: relative;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 999px;
background: #1118;
color: #fff;
/* The transition now animates because --angle is typed */
transition: --angle 800ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Conic gradient used as an animated border via a padding-box mask */
.ring::before {
content: "";
position: absolute;
inset: -2px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(
from var(--angle),
#7aa2ff,
#c084fc,
#7aa2ff
);
/* Show only the 2px border ring, not the fill */
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.ring:hover,
.ring:focus-visible {
--angle: 360deg; /* one full sweep; interpolated smoothly */
}
/* Honour reduced-motion: keep the visual, drop the sweep */
@media (prefers-reduced-motion: reduce) {
.ring { transition: none; }
}
On hover --angle glides from 0deg to 360deg, and because the gradient reads from var(--angle), the colored ring rotates a full turn. Remove the @property block and the ring would jump straight to its final orientation with no rotation.
The key technique: syntax is what unlocks interpolation
The load-bearing descriptor is syntax. It is not documentation — it is the instruction that tells the browser how to compute intermediate values. With syntax: "<angle>" the engine knows to step degrees; with "<number>" it steps a scalar; with "<color>" it interpolates in the appropriate color space; with "<length>" or "<percentage>" it interpolates dimensions. The other two descriptors support that: inherits decides whether the typed value cascades to descendants (set false for component-local effects to avoid surprise inheritance), and initial-value guarantees the property always resolves to a valid value of the declared type, which is required so that animations and calc() never encounter an invalid token.
Variation: counting a number and animating a hue
The same registration unlocks a CSS-only counter and a hue sweep. Two registered properties of different types drive distinct effects from one hover.
@property --count {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
@property --hue {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.stat {
--count: 0;
--hue: 0deg;
/* counter() reads the integer; the hue feeds an hsl() color */
counter-reset: n var(--count);
color: hsl(var(--hue) 80% 60%);
transition: --count 1200ms steps(60, jump-end), --hue 1200ms ease;
}
.stat::after { content: counter(n); }
.stat:hover {
--count: 100; /* ticks up to 100 */
--hue: 280deg; /* glides across the hue wheel */
}
The steps() timing function makes the integer increment in visible ticks rather than blurring through fractional values it cannot display.
Browser support note
@property is supported in Chrome and Edge 85+, Safari 16.4+, and Firefox 128+, giving it broad evergreen coverage by mid-2026. Where it is unavailable the custom property still holds and applies its value correctly — only the animation is lost, so transitions snap instantly instead of interpolating. Treat the motion as a progressive enhancement and, if you want explicit detection, wrap the registration-dependent effect in @supports (background: conic-gradient(from var(--angle), red, blue)) or feature-detect with CSS.supports. The JavaScript CSS.registerProperty() API is an alternative registration path, but it is not needed for the CSS-only approach shown here.
FAQ
Why won't my CSS variable animate even with a transition set?
An unregistered custom property has the universal <custom-ident> type for animation purposes, so the browser treats it as discrete and snaps from the start value to the end value. Register it with @property and a typed syntax such as <angle> or <number>, and the browser will interpolate it smoothly across the transition.
What do syntax, inherits, and initial-value do in @property?syntax declares the value's type so the browser knows how to interpolate it, inherits controls whether the registered property cascades to descendant elements, and initial-value supplies a guaranteed-valid fallback starting value. All three descriptors are required for the @property rule to be valid.
Can I register a custom property entirely in CSS without JavaScript?
Yes. The @property at-rule registers the property purely in CSS, which is all the CSS-only animation needs. The JavaScript CSS.registerProperty() API is an equivalent alternative but is not required.
Which browsers support @property?@property is supported in Chrome and Edge 85+, Safari 16.4+, and Firefox 128+. Where it is unavailable the property still holds its value but changes apply instantly rather than animating, so guard the motion as an enhancement.
Related
- CSS Custom Properties Architecture — the parent guide on token structure, cascade layers, and theming.
- Fluid spacing tokens driving transition durations — extend typed tokens into proportional motion.
- CSS Transition Timing Functions — pick the easing curve for animated properties.
- Reducing motion preferences in CSS — collapse registered-property animations when requested.
- Container query units (cqi, cqb) explained — feed container-relative units into registered properties for layout-aware motion.
Related articles
More pages in the same section.