@starting-style: Transitions From display: none At Last
Why did showing an element never transition? Because transitions interpolate between two rendered states, and a display: none element has no rendered state - it appears, the browser sees only the final styles, nothing to animate from. Every toast and dropdown ever built worked around this with rAF double-frames or keyframe animations. The platform finally fixed the root cause:
.toast {
transition: opacity 0.25s, translate 0.25s, display 0.25s allow-discrete;
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 12px; /* the "before first render" state */
}
}
.toast[hidden] {
display: none;
opacity: 0;
translate: 0 12px;
}
@starting-style declares the styles an element transitions from when it first renders (or re-renders after display: none). Remove the hidden attribute and the toast fades up from nothing - a real entry transition, no JavaScript choreography.
The exit half: allow-discrete
Exit was the other half of the problem: setting display: none removes the element immediately, killing any fade mid-frame. transition-behavior: allow-discrete (the allow-discrete keyword in the shorthand above) changes when discrete properties flip during a transition - display waits until the end, so the opacity fade completes and then the element stops rendering. Entry and exit, both in pure CSS, both keeping display's semantics (no keyboard-focusable invisible elements, unlike the old opacity/visibility-only approach).
Where this sings: dialogs and popovers
dialog {
transition: opacity 0.2s, scale 0.2s,
display 0.2s allow-discrete, overlay 0.2s allow-discrete;
opacity: 1; scale: 1;
@starting-style { opacity: 0; scale: 0.96; }
}
dialog:not([open]) { opacity: 0; scale: 0.96; }
dialog::backdrop { transition: background-color 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete; }
Native dialogs and popovers toggle display internally, which made them notoriously hard to animate - this combo is their designed-for solution. The overlay property in that transition list is top-layer-specific: it keeps the element in the top layer until the exit finishes.
Rules and support
- @starting-style applies on every first render - including initial page load if the element starts visible. Scope it to state-toggled components.
- Nesting it inside the rule (as above) inherits the selector context cleanly - the standalone at-rule form works too.
- Support: Chromium 117+, Firefox 129+, Safari 17.5+ (dialog/popover pieces slightly later in places). Older browsers simply snap - which is what they did before, so this is pure progressive enhancement with zero fallback code.
The double-rAF trick can finally retire. Element lifecycle animation is now a stylesheet concern, which is where it always belonged.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.