The short version
- CSS @property registers a custom property with a syntax, an inheritance rule and an initial value, which tells the browser what type of value it holds.
- Browsers cannot transition a gradient directly, because background-image is not interpolable, but they can transition a typed custom property used inside the gradient.
- Typed properties such as <color>, <angle>, <percentage> and <length> animate smoothly; a property with the universal * syntax only flips between values.
- The @property rule is supported in current Chrome, Edge, Safari and Firefox, and browsers that ignore it simply show the gradient without motion.
- Animating a custom property inside a background repaints the element on every frame, so keep the animated area modest and respect prefers-reduced-motion.
On this page
CSS @property registers a custom property with a type, so the browser knows that --angle holds an angle or --tint holds a colour. Once it knows the type, it can interpolate between two values, which means you can transition and animate things CSS normally refuses to move: gradient colours, gradient angles and the position of a colour stop. Without registration, a custom property is just a string, and a string can only jump from one value to the next.
Why gradients will not transition on their own
Try transition: background 0.4s on a button whose hover state swaps one linear-gradient() for another and nothing eases: the new gradient snaps in. Gradients are images, and the specification treats background-image as not interpolable, so the browser has no way to blend one image into a different one.
The usual workaround is to oversize the background and slide background-position, which is how most animated CSS gradients are built. It works, but it only moves a fixed gradient around. You cannot change a colour, rotate an angle or push a stop along. Registered properties change that, because the thing being animated is no longer the image but a typed number or colour inside it. The browser recalculates the gradient on every frame from the current value.
Unregistered --tint
- Stored as a string of tokens
- Transitions jump halfway through
- Inherits by default
- An invalid value breaks the declaration at computed time
Registered with @property
- Parsed as a real colour
- Transitions interpolate smoothly
- Inheritance is your choice
- An invalid value falls back to the initial value
The @property syntax
An @property rule has three descriptors. syntax says what type of value is allowed, inherits says whether children receive the value, and initial-value is used when nothing else sets it. All three are required, except that initial-value may be left out when the syntax is *. If any required part is missing or wrong, the browser ignores the whole rule, silently.
@property --tint {
syntax: '<color>';
inherits: false;
initial-value: #7c3aed;
}
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --stop {
syntax: '<percentage>';
inherits: false;
initial-value: 40%;
}The initial value must be computationally independent, which in practice means absolute units: 0deg, 40%, 12px and hex colours are fine, while 2em or var(--x) are not. Setting inherits: false is usually what you want for animation, and it spares the browser from pushing the value down the tree.
<color>Animates smoothly
Good for
<angle>Animates smoothly
Good for
<percentage>Animates smoothly
Good for
<length>Animates smoothly
Good for
<number>Animates smoothly
Good for
<integer>Animates smoothly
Good for
*No, it flipsGood for
'<length> | <percentage>', or a list with +.Animate a gradient colour on hover
This is the smallest useful example. The button uses --tint as its second colour, and the hover state only changes --tint. Because the property is registered as a colour, the transition eases between violet and coral instead of snapping.
@property --tint {
syntax: '<color>';
inherits: false;
initial-value: #7c3aed;
}
.button {
background: linear-gradient(120deg, #1e1b4b 0%, var(--tint) 100%);
transition: --tint 400ms ease;
}
.button:hover {
--tint: #fb7185;
}For more ways to treat buttons, see gradient button CSS and CSS gradient hover effects.
Rotate a conic gradient border
A favourite use of @property is a border that seems to travel around a card. A conic gradient starts from an angle, so registering that angle and animating it from 0deg to 360deg spins the colours. Two backgrounds do the work: the card colour clipped to the padding box, and the conic gradient clipped to the border box.
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.card {
border: 2px solid transparent;
border-radius: 16px;
background:
linear-gradient(#0f0b1e, #0f0b1e) padding-box,
conic-gradient(from var(--angle), #22d3ee, #7c3aed, #f472b6, #22d3ee) border-box;
animation: spin 6s linear infinite;
}
@keyframes spin {
to { --angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}conic-gradient(from 0deg, #22d3ee, #7c3aed, #f472b6, #22d3ee)Move a colour stop
A registered percentage lets a stop slide. Use it for a progress fill, a highlight that sweeps across a heading, or a horizon that rises on scroll. Here --stop moves the point where the gradient turns from ink to teal.
@property --stop {
syntax: '<percentage>';
inherits: false;
initial-value: 20%;
}
.meter {
background: linear-gradient(90deg, #0d9488 0%, #0d9488 var(--stop), #0f172a var(--stop));
transition: --stop 600ms ease-out;
}
.meter[data-done] {
--stop: 100%;
}Registering from JavaScript
CSS.registerProperty() does the same job from a script. It is handy when a design system registers its properties at runtime, or when a value only becomes known after load. Registering the same name twice throws an error, so guard it.
if ('registerProperty' in CSS) {
try {
CSS.registerProperty({
name: '--angle',
syntax: '<angle>',
inherits: false,
initialValue: '0deg',
})
} catch {
// Already registered, which is fine.
}
}Browser support, fallbacks and performance
@property works in current Chrome, Edge, Safari and Firefox; Firefox was the last to add it, in version 128. Check caniuse if you support older devices. A browser that does not understand the rule treats --angle as an ordinary custom property, so your gradient still draws at its starting value and simply does not move. That is a fine fallback, provided the static frame looks finished.
- 1
Register before you use
Put every
@propertyrule at the top level, outside media queries, so it applies everywhere. - 2
Use typed syntaxes
Choose
<color>,<angle>or<percentage>. Avoid*for anything you plan to animate. - 3
Transition the property
Write
transition: --tint 400ms, naming the custom property rather thanbackground. - 4
Respect reduced motion
Stop looping animations under
prefers-reduced-motion: reduce, as described in prefers reduced motion. - 5
Check the still frame
Turn the animation off and make sure the resting gradient still looks intentional.
If the motion you want lives on social posts or video rather than a web page, a Gradiently Mark already carries its own light and slow motion, and Pro exports it as MP4, WebM or GIF where the browser supports recording, without writing keyframes. For everything on the web, @property is the cleanest tool you have. The MDN reference lists every descriptor.
Questions people ask
What does CSS @property do?
It registers a custom property with a type, an inheritance rule and an initial value. Knowing the type lets the browser validate the value and interpolate it in transitions and animations.
Can you animate a CSS gradient?
Not directly, because background-image is not interpolable. Register the colours, angle or stop positions with @property, use them inside the gradient, and animate those properties instead.
Why is my @property animation not working?
Usually a descriptor is missing, the initial value uses a relative unit, the syntax is *, or the transition names background instead of the custom property. Any invalid descriptor makes the browser ignore the whole rule.
Does Firefox support @property?
Yes. Firefox added @property in version 128, so it now works in every major current browser.
Should inherits be true or false?
Use false for most animated values. It keeps the value on the element where you set it and avoids extra work for the browser.
Written by Gradiently
The team behind Gradiently, a design tool built around Marks: living gradients that make everything you design look like yours.
See our profile