Entry and exit animations
Animate elements as they appear and disappear — including to and from display: none and the top layer — with @starting-style and transition-behavior: allow-discrete, no JavaScript timers.
What it is
Two CSS features let an element animate as it appears and disappears — including transitions to and from display: none and the browser top layer that popovers and <dialog> render in.
@starting-styledeclares the styles an element starts from the first time it is rendered (or re-added to the top layer). Without a “before” value the browser has nothing to transition out of, so the element simply snaps in.transition-behavior: allow-discretelets discrete properties such asdisplayandoverlaytake part in a transition. The browser defersdisplay: noneuntil the animation finishes instead of cutting the element instantly.
[popover] {
opacity: 0;
transition:
opacity 200ms ease,
display 200ms allow-discrete;
}
[popover]:popover-open {
opacity: 1;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
}
}
Why it matters
- No JavaScript timing dance. Animating an element in from
display: none, or out before removal, used to require toggling classes acrossrequestAnimationFrame, listening fortransitionend, orsetTimeoutto defer removal. This is all declarative now, and the browser owns the timing — fewer race conditions, no orphaned timers. - Exit animations work without script. The element stays visible for the duration of the transition, then the browser applies
display: none. Popovers and modal dialogs animate out of the top layer the same way, via theoverlayproperty. - It composes with the platform. It pairs directly with the Popover API,
<dialog>, and invoker commands rather than re-implementing show/hide logic in JavaScript.
How to implement
Put the transition on the base rule, give the visible state its target values, and put the initial state inside @starting-style. Add allow-discrete to any display or overlay transition. Gate the motion behind prefers-reduced-motion so users who ask for less movement get an instant swap.
@media (prefers-reduced-motion: no-preference) {
.toast {
transition:
transform 200ms,
display 200ms allow-discrete;
}
@starting-style {
.toast.is-open {
transform: translateY(1rem);
}
}
}
Common mistakes
- Omitting
allow-discrete. Without itdisplayflips instantly and the element pops in and out with no transition. - A
@starting-stylerule that does not match the visible state. The starting selector must target the same element and state as the open rule, or there is no “before” value to animate from. - Animating unconditionally. Ignoring
prefers-reduced-motionforces motion on people who have opted out.
Verification
- Toggle a popover, dialog, or toast: it animates both in and out rather than snapping.
- In DevTools, confirm the element keeps its
displayvalue until the exit transition ends. - With
prefers-reduced-motion: reduceactive, transitions are suppressed and the change is instant. @starting-styleandtransition-behavior: allow-discretehave been Baseline (newly available across major browsers) since 2024 — treat the animation as a progressive enhancement.
Related topics
Sources & further reading
- CSS Transitions Level 2 — the @starting-style rule and transition-behavior — W3C CSS Working Group
- MDN — @starting-style — MDN
- MDN — transition-behavior — MDN
- Now in Baseline: animating entry effects — web.dev