In short
CSS @property lets you animate custom properties like gradient angles, @starting-style creates entry animations, and allow-discrete lets you animate to display: none. Why can't you animate a gradient normally? Try this: a button with a gradient, and on hover a different gradient with a transition.
CSS @property lets you animate custom properties like gradient angles, @starting-style creates entry animations, and allow-discrete lets you animate to display: none.
Why can't you animate a gradient normally?
Try this: a button with a gradient, and on hover a different gradient with a transition. The gradient jumps instantly, without any smooth change. Browsers can't blend between two background-image values. Custom properties don't help by default either: to the browser, --angle: 90deg is just text, and it doesn't know how to go from one piece of text to another.
@property fixes this. It registers a custom property and tells the browser its type, like <angle>, <color>, <length> or <percentage>. Once the browser knows --angle is an angle, it can animate it from 0deg to 360deg smoothly. And because the gradient uses the variable, the gradient changes too.
Animating gradients with @property
<div class="glow-card">
<h3>Pro plan</h3>
<p>Everything you need to learn faster.</p>
</div>
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.glow-card {
padding: 24px;
border: 4px solid transparent;
border-radius: 16px;
background:
linear-gradient(white, white) padding-box,
conic-gradient(from var(--angle), #2563eb, #db2777, #f59e0b, #2563eb) border-box;
animation: spin 4s linear infinite;
}
@keyframes spin {
to {
--angle: 360deg;
}
}
The card has a colorful border that slowly rotates around it, like a glowing light. The white padding-box background covers the inside, so only the border shows the gradient. The three descriptors are all required: syntax (the type), inherits (should children get the value?) and initial-value (the starting value).
The same works for transitions. Here a button changes its gradient color smoothly on hover:
@property --start {
syntax: "<color>";
inherits: false;
initial-value: #2563eb;
}
.gradient-btn {
background: linear-gradient(135deg, var(--start), #7c3aed);
color: white;
transition: --start 0.4s;
}
.gradient-btn:hover {
--start: #db2777;
}
On hover, the left side of the gradient fades from blue to pink, instead of jumping.
Entry animations with @starting-style
A transition needs a "before" value and an "after" value. But when an element appears for the first time (added to the page, or changed from display: none), there is no "before", so nothing animates. @starting-style gives the browser that missing "before" state.
.toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 16px 20px;
background: #111827;
color: white;
border-radius: 10px;
opacity: 1;
translate: 0 0;
transition: opacity 0.3s, translate 0.3s;
}
@starting-style {
.toast {
opacity: 0;
translate: 0 20px;
}
}
When a .toast notification is added to the page (for example with JavaScript after saving a form), it starts invisible and 20px lower, and then slides up and fades in. No JavaScript timer or extra class is needed for the animation.
Animating to display: none with allow-discrete
Leaving is the other half of the problem. display is a discrete property: it has no values in between, it is either none or not. Normally it switches immediately, so the element disappears before any fade-out can be seen. transition-behavior: allow-discrete tells the browser to wait: keep the element visible during the transition and switch display at the very end.
Here is a popover menu that fades and scales both in and out. A popover is a built-in HTML feature for menus and tooltips: the button opens and closes it without JavaScript.
<button popovertarget="user-menu">My account</button>
<div id="user-menu" popover>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Log out</a>
</div>
#user-menu {
padding: 12px;
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgb(0 0 0 / 0.2);
opacity: 0;
scale: 0.95;
transition:
opacity 0.2s,
scale 0.2s,
display 0.2s,
overlay 0.2s;
transition-behavior: allow-discrete;
}
#user-menu:popover-open {
opacity: 1;
scale: 1;
}
@starting-style {
#user-menu:popover-open {
opacity: 0;
scale: 0.95;
}
}
Click "My account" and the menu grows and fades in. Click outside or press Escape and it shrinks and fades out, and only then becomes display: none. Three pieces work together: the closed style (opacity 0), the open style (:popover-open), and @starting-style for the very first frame. Adding display and overlay to the transition with allow-discrete keeps the menu visible and on top while it closes.
The same pattern works for a modal <dialog>, opened with dialog.showModal(): use dialog[open] for the open state and you can also fade its ::backdrop.
Note:
@property,@starting-styleandtransition-behaviorwork in all current major browsers. Theoverlayproperty is Chromium-only for now; other browsers ignore it, and the close animation may simply be shorter there. That is a safe enhancement.
Common mistake: Putting
@starting-stylebefore the normal rule with the same selector. It has the same specificity as the normal rule, so the later one wins. Write@starting-styleafter the element's normal styles.
More details on MDN's @starting-style page.
Practice exercise
Build a "Cookie settings" <dialog> with a close button. Make it fade and slide down when it opens, fade out when it closes, and fade its ::backdrop from transparent to a dark tint. Then give the "Save" button inside it an animated gradient border using @property.
Hint: style dialog[open] and dialog[open]::backdrop, add both to @starting-style, and include display and overlay in the transition with allow-discrete.
Key takeaways
@propertyregisters a custom property with a type so it can be animated.- Animate a registered
--angleor--colorto animate gradients. @starting-styledefines the "before" state for elements that just appeared.transition-behavior: allow-discreteletsdisplayswitch at the end of a transition.- Popovers and dialogs can now animate both open and closed with CSS only.