10LOC
Chrome APIsintermediate

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:

  • .toast starts display: none, fully transparent, and offset — its normal resting (hidden) state. Its transition list includes display with allow-discrete, which is required for display to 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-style block 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 open attribute (a click handler, a server-rendered attribute, whatever) — @starting-style only replaces the "wait a frame, then flip a second class" trick, not the triggering logic itself.

Gotchas

  • @starting-style only 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-style needed.
  • Any property you set in @starting-style also has to be listed in the element's transition property, or it's just ignored.
  • This is a transitions feature, not an animations one — @keyframes animations don't need @starting-style, since a 0% 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 makes display/content-visibility transitionable at all; almost always paired with @starting-style for entrance animations, as here.