Entry and exit animations
A transition from the lesson "Transitions" animates a change of value. But an element that has just appeared had no "previous" value — there is nothing to transition from. And display: none does not animate at all: the element vanishes instantly. Modern CSS closes both gaps.
@starting-style: where to start
The @starting-style rule sets the styles an element starts from at the moment it appears. The browser takes them as the "previous" value and runs a transition to the regular styles. Press "Reset" above the example to see it again:
<style>
.note {
padding: 12px;
background: honeydew;
transition: opacity 1s, translate 1s;
}
@starting-style {
.note { opacity: 0; translate: 0 20px; }
}
</style>
<div class="note">I slid up smoothly</div>Without @starting-style the block would just appear in place at once. Here translate is a separate shift property, the same as transform: translate().
A transition from display: none
display switches abruptly: there is no "half hidden" value. Such properties are called discrete. The keyword allow-discrete (in full — transition-behavior: allow-discrete) lets them take part in a transition: when hiding, display changes to none at the very end, when the opacity has already reached zero:
<style>
.box {
padding: 12px;
margin-top: 8px;
background: lavender;
transition: opacity 0.5s, display 0.5s allow-discrete;
}
.box.hidden { display: none; opacity: 0; }
@starting-style {
.box { opacity: 0; }
}
</style>
<button onclick="document.querySelector('.box').classList.toggle('hidden')">Show / hide</button>
<div class="box">I appear smoothly and disappear smoothly</div>@starting-style handles appearing, and allow-discrete handles disappearing. Both are needed.
A dialog and a popover
<dialog> windows and popover blocks from the lesson "Disclosure widgets and dialogs" are also hidden with display: none. The scheme is the same; besides display, the overlay property is added to the transition — it keeps the window above the page while the closing animation runs:
<style>
body { min-height: 200px; }
dialog {
opacity: 0;
scale: 0.9;
transition: opacity 0.3s, scale 0.3s, display 0.3s allow-discrete, overlay 0.3s allow-discrete;
}
dialog[open] { opacity: 1; scale: 1; }
@starting-style {
dialog[open] { opacity: 0; scale: 0.9; }
}
</style>
<button onclick="document.querySelector('#hi').showModal()">Open the window</button>
<dialog id="hi">
<p>The window opened smoothly.</p>
<form method="dialog"><button>Close</button></form>
</dialog>For a popover, write [popover]:popover-open instead of dialog[open].
Height auto: interpolate-size
Another long-standing problem is a transition to height: auto. The browser could not compute in-between values between a number and the word auto, and the block opened with a jump. The line interpolate-size: allow-keywords on :root allows such transitions on the whole page. Hover over the block:
<style>
:root { interpolate-size: allow-keywords; }
.more {
height: 24px;
overflow: hidden;
padding: 8px;
background: lightyellow;
transition: height 0.4s;
}
.more:hover { height: auto; }
</style>
<div class="more">Hover — I will open.<br>The second line.<br>The third line.<br>The fourth line.</div>At the time of writing interpolate-size works in Chrome and Edge; in other browsers the block simply opens at once, without animation. Check current support on caniuse.com.
Support
@starting-style and allow-discrete are supported in all major browsers since 2024. The overlay property is not everywhere: without it the window may close without animation, but it still closes. All these techniques are progressive enhancement: an old browser shows and hides the element instantly, as before.
Common mistake
@starting-style before the main rule. The selectors have the same specificity, so the lower one wins: the regular style overrides the starting one, and the element appears without animation. Press "Reset":
<style>
@starting-style { .bad { opacity: 0; } }
.bad, .good { opacity: 1; padding: 8px; margin: 4px 0; background: mistyrose; transition: opacity 1.5s; }
@starting-style { .good { opacity: 0; } }
</style>
<div class="bad">Starting style above — I appear at once</div>
<div class="good">Starting style below — I fade in smoothly</div>Write @starting-style after the element's rule, or nest it right inside the rule.