@property: the one that makes custom properties actually animate
CSS custom properties can't animate because the browser doesn't know their type. @property declares the type. After that, they animate, transition and interpolate like native values.
At some point you tried to animate a CSS custom property with a transition and nothing happened:
.element {
--glow-opacity: 0;
box-shadow: 0 0 20px rgba(196, 82, 42, var(--glow-opacity));
transition: --glow-opacity 300ms ease;
}
.element:hover {
--glow-opacity: 0.8;
}
The browser jumped from 0 to 0.8. No interpolation. The transition did nothing.
The reason: CSS custom properties are opaque to the browser. Without knowing the type, the browser can’t interpolate. Interpolating between “apple” and “orange” is undefined. So it skips it.
@property registers the custom property with a type:
@property --glow-opacity {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
.element {
box-shadow: 0 0 20px rgba(196, 82, 42, var(--glow-opacity));
transition: --glow-opacity 300ms ease;
}
.element:hover {
--glow-opacity: 0.8;
}
Now the browser knows --glow-opacity is a number. It interpolates. The transition works.
Browser support: Baseline 2024. Chrome 85+ (October 2020), Safari 16.4+ (March 2023), Firefox 128+ (June 2024). MDN reference.
A rotating gradient without JavaScript
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@keyframes spin-gradient {
to { --gradient-angle: 360deg; }
}
.spinning-border {
background: conic-gradient(
from var(--gradient-angle),
var(--rust, #c4522a) 0deg,
var(--paper, #faf7f4) 90deg,
var(--rust, #c4522a) 180deg,
var(--paper, #faf7f4) 270deg,
var(--rust, #c4522a) 360deg
);
animation: spin-gradient 4s linear infinite;
}
Without @property, --gradient-angle can’t be animated — the browser doesn’t know it’s an angle. With it, the conic-gradient spins smoothly. No JavaScript. No requestAnimationFrame.
@property --gradient-angle { syntax: '<angle>' }No JS. No rAF. CSS animates the angle directly.
A progress ring
@property --ring-progress {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
.progress-ring {
--ring-progress: 0;
background: conic-gradient(
var(--accent) calc(var(--ring-progress) * 1%),
var(--surface-muted) 0
);
border-radius: 50%;
width: 80px;
height: 80px;
transition: --ring-progress 600ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
// Update progress — one property, eased transition
ring.style.setProperty('--ring-progress', '75');
The conic-gradient updates with an eased transition. The visual is a smooth-filling arc. One property update, one line of JavaScript.
The syntax values you’ll actually use
@property --my-color { syntax: '<color>'; inherits: true; initial-value: black; }
@property --my-length { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --my-number { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --my-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
@property --my-percent { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
The inherits flag matters: if true, the property follows the CSS cascade like color or font-size. If false, it doesn’t inherit from parent to child — useful for properties that are component-local.
The four years we spent waiting for Firefox is, in the way of these things, already starting to feel like it happened to someone else.
Ship it.