CSS @starting-style Explained: Entry Transitions in CSS
CSS @starting-style defines an element's before-state on first render, so transitions can animate elements as they appear, even from display: none.
@starting-style is a CSS at-rule that defines the styles an element should transition from the first time it gets a style — when it’s added to the page or when it switches from display: none to visible. Ordinary CSS transitions can’t animate an element’s appearance because there’s no “previous” style to start from. @starting-style supplies that missing starting point, which makes fade-in and slide-in entry effects possible with pure CSS, no JavaScript class toggling required.
The problem it solves
CSS transitions run when a property changes from one computed value to another. That works fine for hover states, where an element already exists with a known style. It breaks down in two common situations:
- An element is inserted into the DOM. It’s rendered with its final styles on its very first frame. There’s no prior value, so nothing transitions.
- An element goes from
display: noneto displayed. Elements withdisplay: nonedon’t have computed styles for rendering, so again the browser treats the first visible frame as the starting point.
The classic workaround was JavaScript: insert the element with an “initial” class, force a reflow or wait a frame with requestAnimationFrame, then swap to a “visible” class so the transition fires. It works, but it’s fiddly and easy to get wrong. CSS @keyframes animations can handle entry, but they don’t reverse smoothly when interrupted the way transitions do.
Basic syntax
You write @starting-style with the same selectors you’d use normally, describing the “before” state:
.toast {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
@starting-style {
.toast {
opacity: 0;
transform: translateY(1rem);
}
}
When a .toast element is added to the page, the browser first applies the starting styles, then transitions to the regular ones. The element fades and slides up into place.
You can also nest it inside the rule it applies to, using CSS nesting:
.toast {
opacity: 1;
transition: opacity 0.3s;
@starting-style {
opacity: 0;
}
}
Starting styles only apply on an element’s first style update. They don’t interfere with later transitions, such as hover effects.
Animating from display: none
Entry is half the story. To animate an element that toggles between display: none and visible — popovers, dialogs, dropdowns — you need two more pieces.
First, display itself must be allowed to participate in the transition. It’s a discrete property: it can’t interpolate, it can only flip. The transition-behavior: allow-discrete declaration (or the allow-discrete keyword in the transition shorthand) tells the browser to flip discrete properties at the right moment. When showing an element, display flips at the start so the element is visible throughout the fade-in. When hiding, it flips at the end so the fade-out can finish before the element disappears.
Second, @starting-style provides the entry state:
.panel {
display: none;
opacity: 0;
transition:
opacity 0.25s,
display 0.25s allow-discrete;
}
.panel.open {
display: block;
opacity: 1;
}
@starting-style {
.panel.open {
opacity: 0;
}
}
Adding the open class now fades the panel in, and removing it fades the panel out before display: none takes effect.
Popovers and dialogs
This pattern is especially useful with the Popover API and the <dialog> element, both of which toggle between hidden and shown states and render in the top layer. The top layer adds one more wrinkle: when the element closes, it’s removed from the top layer immediately, which can cut the exit animation short. Transitioning the overlay property with allow-discrete keeps it in the top layer until the transition finishes:
[popover] {
opacity: 0;
transform: scale(0.95);
transition:
opacity 0.2s,
transform 0.2s,
overlay 0.2s allow-discrete,
display 0.2s allow-discrete;
}
[popover]:popover-open {
opacity: 1;
transform: scale(1);
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
The same approach works for a dialog’s ::backdrop, which you can fade in by giving it its own starting style.
Entry vs exit states
A common source of confusion: @starting-style only describes the entry state. The exit state is whatever the element’s styles are once it’s no longer open — in the popover example, the base [popover] rule. That means entry and exit can be different. You might slide a menu in from the left and fade it out in place, simply by giving the base rule and the starting style different values.
Things to watch out for
- Specificity and order still apply. Starting styles cascade like any other rules. If your
@starting-styleblock has lower specificity than the rule setting the final value, the starting value may never apply. Matching the selector exactly, or nesting the block, avoids surprises. - Not every property should animate. Stick to
opacityandtransformwhere you can; they’re the cheapest to animate. Transitioning layout properties likeheightortopforces layout work on every frame. - Respect reduced motion. Wrap motion-heavy entrances in a
prefers-reduced-motionquery, or fall back to a simple fade. - It’s progressive enhancement. Browsers that don’t support
@starting-stylesimply ignore it, and elements appear instantly — a safe fallback. You can detect support with@supportsif you need to branch.
When to use something else
@starting-style is ideal for entrance and exit effects on individual elements. For animating between two whole page or component states — such as a list reordering or a page navigation — the View Transitions API is a better fit. For looping or multi-step motion, @keyframes remains the right tool.
The takeaway
@starting-style gives an element a “before” style for its very first render, so normal CSS transitions can animate it onto the page. Pair it with transition-behavior: allow-discrete on display (and overlay for top-layer elements) to animate both entry and exit of popovers, dialogs, and toggled panels in pure CSS. Keep the animated properties cheap, respect reduced-motion preferences, and treat it as progressive enhancement.
Tagged
Keep reading
Takina · · 4 min read Container Query Units: cqw, cqh, and cqi Explained
Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.
Takina · · 4 min read CSS Relative Color Syntax: Deriving Colors from Colors
Relative color syntax lets CSS derive a new color from an existing one by adjusting its channels directly, no preprocessor or JS required.
Takina · · 4 min read What Is CSS Subgrid? Nested Grid Alignment Explained
CSS subgrid lets a nested grid item inherit its parent's track sizing, so children can align to the same columns or rows across unrelated containers.