Entry Animations Without a Mount Class: @starting-style and Discrete Transitions
@starting-style gives a first-render element a state to transition from, and allow-discrete lets display flip mid-transition. Together they replace the mount-class dance.
You insert a node, add a class, and expect a fade. Nothing animates. The usual diagnosis is a race — the class landed in the same frame as the insert, the browser never saw the "before" state. CSS transitions provide a way to control animation speed when changing CSS properties[13], and a transition only exists when there are two computed values to interpolate between. On the frame an element is first rendered, there is only one.
A Transition Needs a Before and an After
In Level 1 of the transitions spec, a transition could only start during a style change event for elements that already had a before-change style established by the previous style change event[5] — which excludes anything that was not being rendered last time around. The fix is a grouping rule whose rules establish the styles to transition from when the previous style change event did not establish a before-change style[5] — or, as the web-features description puts it, an at-rule that sets the starting values for properties that are transitioning when the target element's style is first updated[10]. That means @starting-style rules only apply to elements that were not rendered or part of the DOM during the previous style change event[5]. MDN states the practical version: transitions are by default not triggered on an element's initial style update, or when its display type changes from none to another value[1].
Two Syntaxes, One Cascade Weight
@starting-style works as a standalone block containing rulesets, or nested inside an existing ruleset where it holds declarations for the elements that ruleset already selected[1]. Both forms feed the same cascade step, and that is the first trap: the at-rule and the "original rule" have the same specificity, so the starting block has to come after the rule it modifies, or the original styles will override the starting styles[1]. The spec is explicit that rules inside @starting-style cascade as any other grouped style rules without introducing any new ordering to the cascade[5], and that they do not apply to the after-change style at all[5].
[popover]:popover-open {
opacity: 1;
transform: scaleX(1);
/* Nested form: same specificity as the outer rule, so nesting
is what keeps the ordering unambiguous. */
@starting-style {
opacity: 0;
transform: scaleX(0);
}
}Discrete Properties Are Switches
Some properties cannot interpolate at all. Their animation type is discrete: values are not additive, and interpolation swaps from the start value to the end value at 50%[4]. display, visibility, and mix-blend-mode are the familiar examples, and unregistered custom properties are discrete too[4]. By default a transition is simply not started for them. The transition-behavior property specifies whether transitions will be started for properties whose animation behavior is discrete[2], and its allow-discrete value turns that on — discrete-animated properties then flip between two values 50% through animating between the two[2].
The display: none Exception
There is a carve-out worth memorizing, because it explains why popover animations look the way they do. When animating to or from display: none or content-visibility: hidden, the browser flips between the two values so that the transitioned content is shown for the entire animation duration[2]. This is the "display animation" capability, described in web-features as the ability to animate elements between display: none and any other display value or between content-visibility: hidden and any other content-visibility value, and it applies to transitions as well as animations[11]. Concretely: animating display from none to block flips to block at 0% of the duration so it is visible throughout[2], and animating back to none flips at 100% so it stays visible throughout[2]. web.dev frames the same behaviour as swapping the values at the 50% point instead of at the 0% point of a transition[6].
The Top Layer Needs Two Extra Names
Popovers and modal dialogs are promoted to the top layer when shown, which sets display: none on the hidden state and makes the exit animation invisible without help. display has to be in the transition list so the element is visible throughout both the entry and exit animations — without it the exit animation would not be visible and the popover would just disappear[1]. The overlay property specifies whether an element appearing in the top layer is actually rendered in the top layer[3], and adding it to the transition list defers the element's removal from the top layer until the animation ends[1]. Note that overlay can only be set by the browser[3], and its animation is unusual: the visible state will always be shown for the full duration of the transition, regardless of whether it is the start or end state[3].
Three States, Not Two
An element transitions from its @starting-style styles when it is first rendered in the DOM, or when it transitions from display: none to a visible value; when it transitions back from that visible state it no longer uses the starting styles and instead transitions to whatever exists for its default state[1]. That gives three style states to manage — starting-style, transitioned, and default[1] — and the "to" and "from" transitions can legitimately differ.
The Ordering Trap in the Shorthand
allow-discrete can be written as a standalone declaration or inside the transition shorthand, and when it is inside the shorthand it has no impact on regular animatable properties[2]. But position matters: if you apply transition-behavior: allow-discrete before the transition shorthand where you are applying the transition, timing, and easing functions, the browser will ignore the transition-behavior[6]. The shorthand resets the longhand. For older engines, the defensive pattern is to declare transition twice — the first instance without allow-discrete, so the card's other properties still transition in browsers that don't support transition-behavior[2].
.toast {
opacity: 1;
translate: 0 0;
/* First pass: works everywhere, animates the interpolable parts. */
transition: opacity 0.3s, translate 0.3s;
/* Second pass: adds the discrete flip where supported. */
transition: opacity 0.3s, translate 0.3s, display 0.3s allow-discrete;
}
@starting-style {
.toast {
opacity: 0;
translate: 0 1rem;
}
}
.toast[hidden] {
display: none;
opacity: 0;
translate: 0 1rem;
}What Degrades, and What Still Doesn't
@starting-style reached Baseline Newly available on 6 August 2024, with Firefox 129 closing the gap: Chrome 117, Edge 117, Firefox 129, Safari 17.5[6]. Browser-compat-data records the same floors — Chrome 117, Firefox 129, Safari 17.5[8] — and for transition-behavior Chrome 117, Firefox 129, Safari 17.4[9]. Without these features the elements that are animating into the top layer or from a display: none style will simply appear on your page without the transition[6], which is a benign failure mode rather than a broken layout.
The exception is overlay. Its compat entry still lists Firefox false and Safari false[14], MDN labels it limited availability because it does not work in some of the most widely-used browsers[3], and web.dev's own note is blunt: styling exit animations requires animating display: none, and animating a ::backdrop pseudo-element requires overlay — neither is Baseline yet[6]. Treat overlay as progressive enhancement, and note that the same BCD file records Firefox as false for the display transition itself, with Safari only adding it in 18[9].
Where to Reach For It
The pattern is a good fit for anything that mounts into view without a user-visible prior state: popovers, dialogs, toasts, and list items appended by a script. It is not a general animation system — @starting-style is only relevant to CSS transitions[1], so keyframe-based animations do not need it, and the Chrome article makes the same split, covering exit animations separately and noting that entry animations are where the @starting-style rule is required[7]. And because entry effects are exactly the class of motion that needs an off switch, gate the translate and scale parts behind @media (prefers-reduced-motion: no-preference) — the media feature exists to detect a user setting that minimizes non-essential motion[12], and animations such as scaling or panning large objects can be vestibular motion triggers[12]. Let opacity carry the effect alone in the reduced case.
Sources
[1] https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style — MDN — @starting-style CSS at-rule
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior — MDN — transition-behavior CSS property
[3] https://developer.mozilla.org/en-US/docs/Web/CSS/overlay — MDN — overlay CSS property
[4] https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties — MDN — Animatable CSS properties
[5] https://drafts.csswg.org/css-transitions-2 — CSS Transitions Module Level 2 — Editor's Draft
[6] https://web.dev/blog/baseline-entry-animations — web.dev — Now in Baseline: animating entry effects
[7] https://developer.chrome.com/blog/entry-exit-animations — Chrome for Developers — Four new CSS features for smooth entry and exit animations
[8] https://github.com/mdn/browser-compat-data/blob/main/css/at-rules/starting-style.json — mdn/browser-compat-data — css/at-rules/starting-style.json
[9] https://github.com/mdn/browser-compat-data/blob/main/css/properties/transition-behavior.json — mdn/browser-compat-data — css/properties/transition-behavior.json
[10] https://github.com/web-platform-dx/web-features/blob/main/features/starting-style.yml — web-features — starting-style.yml
[11] https://github.com/web-platform-dx/web-features/blob/main/features/display-animation.yml — web-features — display-animation.yml
[12] https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion — MDN — prefers-reduced-motion CSS media feature
[13] https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions/Using_CSS_transitions — MDN — Using CSS transitions
[14] https://github.com/mdn/browser-compat-data/blob/main/css/properties/overlay.json — mdn/browser-compat-data — css/properties/overlay.json