CSS@starting-style (entry animations)

@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-style can 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 (via transition-behavior: allow-discrete), since entry/exit animations usually involve a display: none boundary.

  • 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).