CSS @starting-style for Entry Transitions
The CSS @starting-style at-rule solves a long-standing
limitation in web development by enabling smooth entry transitions for
elements appearing on a page, entering the top layer, or toggling from
display: none. Historically, CSS transitions required an
element to already exist in the DOM with an established style state,
making it impossible to animate an element immediately upon mounting or
display toggling without JavaScript timing hacks. This article explores
the mechanics of @starting-style, its role in entry and
exit lifecycles, and how it pairs with discrete property transitions to
create seamless UI animations.
The Problem: Animating Initial Rendering
CSS transitions work by interpolating between two distinct states
over a duration. When an element is added to the DOM or changed from
display: none to another display value (such as
block or flex), the browser evaluates its
initial styles and renders them instantly. Because there is no
previously rendered frame to compare against, standard transition rules
fail to trigger, resulting in an abrupt visual pop rather than a fluid
animation.
Developers traditionally resolved this limitation using JavaScript by rendering the element hidden, forcing a reflow or waiting for an animation frame, and then adding an active class to trigger the transition.
How @starting-style
Works
The @starting-style at-rule provides a native,
declarative way to define the element's style state before its first
style update. When the browser computes the styles for an element that
is becoming visible, it reads the properties within
@starting-style as the base state, immediately
interpolating them toward the element’s standard styles.
/* Base state of the element when visible */
.dialog {
opacity: 1;
transform: scale(1);
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* Starting state applied before initial render */
@starting-style {
.dialog {
opacity: 0;
transform: scale(0.9);
}
}In this example, when .dialog is inserted into the
document, the browser assigns it an opacity of
0 and a scale of 0.9 before
animating it to an opacity of 1 and a
scale of 1.
Pairing with Discrete Transitions for Display Changes
While @starting-style defines the starting values for
properties like opacity or transform,
transitioning from display: none requires an additional
property: transition-behavior: allow-discrete.
By default, discrete properties like display switch
values instantly at the start or end of an animation. By permitting
discrete transitions, you instruct the browser to keep the element
displayed until continuous transitions (like fading out) finish:
.card {
display: block;
opacity: 1;
transition: opacity 0.3s ease, display 0.3s ease;
transition-behavior: allow-discrete;
}
.card.hidden {
display: none;
opacity: 0;
}
@starting-style {
.card {
opacity: 0;
}
}Entry vs. Exit Transitions
It is important to note the specific boundary of
@starting-style:
- Entry Transitions:
@starting-styleapplies only to the entry phase. It defines the state from which an element transitions when moving from not rendered to rendered. - Exit Transitions: Exit transitions do not use
@starting-style. Instead, the exit state is handled by standard class updates (such as an.is-closingclass) or pseudo-classes whiletransition-behavior: allow-discreteensures the element remains visible throughout the closing animation duration.
Top-Layer Elements and Popovers
The @starting-style rule plays a critical role in native
HTML elements such as <dialog> and elements
leveraging the Popover API. When a dialog opens using
.showModal(), it enters the browser's top layer. Combining
@starting-style with the overlay property
allows dialog backdrops and popovers to smoothly fade into view and
dismiss without relying on complex third-party animation libraries.