CSS @starting-style for entry animations with no JS
Published January 22, 2027
.toast {
display: none;
opacity: 0;
translate: 0 12px;
transition: opacity 0.2s ease, translate 0.2s ease, display 0.2s allow-discrete;
}
.toast[open] {
display: block;
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 12px;
}
}What
@starting-style defines the styles a transition should animate from on an element's very first style update — the moment it's added to the DOM, or flips from display: none to rendered — a moment that normally has no "previous style" for a transition to interpolate from at all.
Why it matters
A CSS transition needs two different computed styles to animate between: the one before a change and the one after. That works fine for an element that's already visible — toggle a class, the transition fires. It breaks for an element's first render, because there is no "before" style; the element didn't exist in a rendered state a moment ago. That's why the classic mount animation is a JS dance: add the element with its final styles, then on a following frame (requestAnimationFrame, or a class toggled after insertion) apply the "now animate" class — manufacturing a fake previous state so the transition has something to interpolate from.
@starting-style gives the browser that previous state directly, in CSS, for exactly this one moment. Combined with transition-behavior: allow-discrete, it also covers the display: none → visible case specifically, which is the actual replacement for the JS mount dance most sites still ship.
How it works
The snippet animates a toast notification's entrance:
.toaststartsdisplay: none, fully transparent, and offset — its normal resting (hidden) state. Itstransitionlist includesdisplaywithallow-discrete, which is required fordisplayto be transitionable at all; without it, an unsupported value there is simply invalid and skipped..toast[open]is the visible end-state:display: block, full opacity, no offset.- The
@starting-styleblock nested inside.toast[open]supplies the before values — transparent, offset — for the specific instant this rule starts applying to a previously-not-rendered element. The browser transitions from those values to.toast[open]'s real values automatically. - Nothing here runs JS to trigger the animation. Something still has to add the
openattribute (a click handler, a server-rendered attribute, whatever) —@starting-styleonly replaces the "wait a frame, then flip a second class" trick, not the triggering logic itself.
Gotchas
@starting-styleonly fires on an element's first style update. It's meaningless for an element that's already rendered and just changing state — that case already has a real previous style to transition from, no@starting-styleneeded.- Any property you set in
@starting-stylealso has to be listed in the element'stransitionproperty, or it's just ignored. - This is a transitions feature, not an animations one —
@keyframesanimations don't need@starting-style, since a0%keyframe already defines the starting state explicitly. - Unsupported browsers don't error; the rule is ignored, the element just appears with no transition. Safe to ship without a feature-detection branch.
Browser support: Baseline 2024 — Chrome/Edge 117+, Safari 17.5+, Firefox 129+. Safe to use as the primary mechanism today, no fallback path required.
Related
- The View Transitions API — animates between two full DOM snapshots; heavier-weight and a different tool for a different job (page/state transitions, not a single element's mount).
transition-behavior: allow-discrete— the piece that makesdisplay/content-visibilitytransitionable at all; almost always paired with@starting-stylefor entrance animations, as here.