CSS @starting-style: Animate Elements Entering the DOM
The CSS @starting-style rule provides a native mechanism
to animate elements as they are newly inserted into the Document Object
Model (DOM) or toggled from display: none. Historically,
CSS transitions required an already-rendered starting state to
interpolate values, forcing developers to rely on JavaScript reflow
hacks or @keyframes. This article explains how
@starting-style defines an initial pre-render style state,
how the browser processes it during DOM entry, and how to configure
transitions for smooth mount and unmount animations.
The Problem with Initial Mount Transitions
CSS transitions are designed to animate property changes between two
computed states over time. However, when an element is dynamically
inserted into the DOM or changes from display: none to a
visible display state (such as block or flex),
the browser does not have a previous rendered frame to transition
from.
Without a baseline, the browser instantly computes the element's
final styles on the first paint, causing the element to appear abruptly
without executing any defined CSS transition. In the past, bypassing
this limitation required forcing a browser reflow using JavaScript (such
as accessing element.offsetHeight) or using CSS
@keyframes animations, which are more rigid and harder to
reverse cleanly.
How @starting-style
Functions
The @starting-style at-rule establishes initial style
values for an element before its first style resolution and paint
cycle.
When an element becomes visible, the browser checks if a
@starting-style block applies to it. If present, the engine
uses these properties as the starting point (from-values) and
immediately transitions them to the element's standard styles
(to-values) according to its transition properties.
Under the hood, the process works in three steps:
- Instantiation: The element is appended to the DOM
or changes from
display: none. - Initial Resolution: The browser assigns the
property values declared inside
@starting-styleas the initial computed styles before painting. - Transition Execution: The browser switches the element's styles to its regular rule set, triggering the standard CSS transition pipeline toward the final values.
Basic Syntax and Implementation
@starting-style can be written as a standalone rule
targeting a selector or nested directly inside a CSS rule block.
.card {
opacity: 1;
transform: translateY(0);
transition: opacity 0.4s ease, transform 0.4s ease;
}
/* Define the entry state */
@starting-style {
.card {
opacity: 0;
transform: translateY(20px);
}
}When .card is injected into the DOM via JavaScript, it
instantly initializes at opacity: 0 and
translateY(20px), then smoothly animates to
opacity: 1 and translateY(0).
Animating from
display: none
When animating an element toggled via the display
property, @starting-style must be paired with
transition-behavior: allow-discrete (or the
allow-discrete keyword in the transition
shorthand). Because display is a discrete property, this
flag ensures the element remains rendered as display: block
for the duration of the transition rather than snapping immediately.
.modal {
display: flex;
opacity: 1;
transform: scale(1);
transition: opacity 0.3s ease, transform 0.3s ease, display 0.3s allow-discrete;
}
.modal.is-hidden {
display: none;
opacity: 0;
transform: scale(0.95);
}
@starting-style {
.modal {
opacity: 0;
transform: scale(0.95);
}
}Animating Native Top-Layer Elements
@starting-style seamlessly handles native
browser-managed UI, such as <dialog> elements and
components utilizing the Popover API. Because these elements toggle
between open and closed states, you can define their entry styles
directly alongside their open states:
dialog {
opacity: 0;
transform: scale(0.8);
transition: opacity 0.3s ease, transform 0.3s ease, display 0.3s allow-discrete, overlay 0.3s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: scale(1);
}
/* Entry state when the dialog is opened */
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.8);
}
}In this setup, the dialog element smoothly scales and
fades in upon calling dialog.showModal(), while the
overlay transition keyword ensures proper rendering timing
within the browser's top-layer stack.