@starting-style (Entry Animations)
CSS transitions have always had an awkward blind spot: they animate a property from its previous value to a new one, but an element that is just now appearing — going from
display: none to visible, or being freshly inserted into the DOM — has no "previous value" to transition from. There was never a prior render to read a starting state off of, so the transition had nothing to animate from and simply snapped straight to the final state. @starting-style solves this by letting you explicitly declare what the "before" state should be treated as, purely for the purposes of that first transition.The problem, illustrated
CSS
.popover {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.popover[open] {
opacity: 1;
transform: scale(1);
}
/* Without @starting-style: when .popover goes from not rendered
(e.g. display: none, or not yet in the DOM) to [open], the browser
has no prior frame to transition from — it just appears instantly
at opacity: 1, scale(1). The transition above only fires on
SUBSEQUENT changes, like closing and reopening within the same
render, not on the very first appearance. */Declaring the starting state
@starting-style defines the property values the browser should treat the element as having had, immediately before its first real style update — giving the transition an actual starting point to animate from on entry.CSS
.popover {
opacity: 1;
transform: scale(1);
transition: opacity 0.2s ease, transform 0.2s ease;
/* The "before" state, used only for the very first transition
when this element first begins rendering */
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}
/* display: none also needs to be transitionable for the whole
sequence to work smoothly */
.popover:not([open]) {
display: none;
}Worked example: a fading, scaling popover
HTML
<button popovertarget="info-popover">Show info</button> <div id="info-popover" popover class="popover"> Here's some extra detail, revealed with a smooth entrance. </div>
CSS
.popover {
opacity: 1;
transform: scale(1) translateY(0);
transition: opacity 0.25s ease, transform 0.25s ease, display 0.25s allow-discrete;
@starting-style {
opacity: 0;
transform: scale(0.92) translateY(8px);
}
}
/* The Popover API toggles this automatically, but the display
transition needs allow-discrete so display doesn't just flip
instantly and cut the animation short */
.popover:not(:popover-open) {
display: none;
opacity: 0;
transform: scale(0.92) translateY(8px);
}Note
The exit direction matters too: for the popover to visibly animate OUT (not just snap away), the closed state also needs
opacity: 0 and the scaled-down transform, with display: none transitioning via allow-discrete so the browser keeps rendering the element for the duration of the closing transition instead of removing it from the render tree immediately.@starting-style only affects the very first style update, not every subsequent open/close
If a component is toggled open and closed repeatedly while it stays in the DOM (e.g. hidden with a class rather than removed), ordinary transitions handle those later toggles just fine —
@starting-style specifically exists to cover the one case ordinary transitions can't: the very first appearance, where there is no previous rendered state to transition from.@starting-stylecan be nested inside a rule (as above) or used as a separate top-level block targeting the same selector.It pairs naturally with the newer support for transitioning
display(viatransition-behavior: allow-discrete), since entry/exit animations usually involve adisplay: noneboundary.It works well with the native
<dialog>element and the Popover API, both of which have well-defined open/close moments to animate around.
Note
Before this, animating an entrance reliably required a JavaScript trick: render the element in its "before" state, wait a frame (or use `requestAnimationFrame`), then apply the "after" state so the browser has something to transition from. `@starting-style` moves that entire dance into CSS.
Next
A broader look at the low-level APIs behind features like
@property: [CSS Houdini Overview](/css/css-houdini).