Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

@starting-style: Transitions From display: none At Last

C
Carlo Fontanos
· 2 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading