CSS @starting-style Reference
First-render transitions, display:none entry states, and popover/dialog patterns with ready-to-copy @starting-style CSS
CSS selector for the element to animate — e.g. .toast, .drawer
Animated Properties
1noneTransition
Animated via @starting-style (2 properties):
- •
opacity - •
transform
Generated CSS
/* Entry animation — .notification */
.notification {
/* End state (element is visible) */
opacity: 1;
transform: none;
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
@starting-style {
.notification {
/* Transition start values — element enters FROM these styles */
opacity: 0;
transform: translateY(-8px);
}
}You add a CSS transition to a notification banner — opacity 0.3s ease. You test it: the banner appears, but there is no fade-in. It just pops in at full opacity. The transition only fires when the element changes state, not when it first appears. @starting-style exists for exactly this case: it gives the browser a synthetic “before” state for an element that did not previously exist or was display: none.
Quick Answers
- Use
@starting-stylewhen an element has no previous rendered state: first render,display: none-> visible, popover open, or dialog open. - Do not use
@starting-stylefor ordinary hover, focus, or class toggles on elements that were already rendered. Regular transitions already handle those. - You still need a normal
transitiondeclaration.@starting-styleonly provides the starting values. - For
displaytransitions you needdisplay ... allow-discrete. For popovers and dialogs you often also needoverlay ... allow-discrete. - Safari supports
@starting-style, but closing popover/dialog animations still fall back becauseoverlay allow-discreteis not fully supported there.
Why This Reference (Not MDN)
MDN’s @starting-style documentation covers the syntax but it does not lead with the question most people actually search for: “when do I need @starting-style instead of a normal transition?” This guide starts with that answer, then covers popovers, dialogs, display: none, allow-discrete, and the browser-support caveats that usually trip people up.
What Is the CSS @starting-style At-Rule?
The CSS @starting-style at-rule solves a fundamental problem with CSS transitions: transitions do not fire when an element is first rendered because there is no “before” state to transition from. @starting-style provides that missing initial state, enabling smooth entry animations for elements entering the page.
/* Without @starting-style — element appears instantly, no transition */
.notification {
opacity: 1;
transition: opacity 0.3s ease;
}
/* With @starting-style — element transitions from 0 → 1 */
.notification {
opacity: 1;
transition: opacity 0.3s ease;
}
@starting-style {
.notification {
opacity: 0; /* browser transitions FROM this value */
}
}
@starting-style shipped in Chrome 117+, Edge 117+, Firefox 129+, and Safari 17.5+ — covering the vast majority of modern browsers.
How @starting-style Works
When a CSS transition is computed, the browser needs both a “before” value and an “after” value. For elements that already exist in the DOM, the before value comes from the element’s previous computed style. For elements that are new — just inserted into the DOM or just becoming visible from display: none — there is no previous computed style.
@starting-style provides the “before” values for these cases:
- First render: element is added to the DOM for the first time
- display: none → visible: element transitions from hidden to shown
- Popover open:
[popover]element enters the:popover-openstate - Dialog open:
dialogelement enters the[open]state
The browser transitions FROM the @starting-style values TO the element’s regular computed styles.
Basic @starting-style Usage
/* Regular styles — the "end state" of the transition */
.card {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.4s ease,
transform 0.4s ease;
}
/* @starting-style — the "start state" of the transition */
@starting-style {
.card {
opacity: 0;
transform: translateY(-12px);
}
}
When .card is added to the DOM, the browser reads the @starting-style values as the start, computes opacity: 1; transform: translateY(0) as the end, and runs the transition.
When NOT to Use @starting-style
@starting-style is not a replacement for ordinary CSS transitions. If an element is already on the page and you are toggling a class, :hover, :focus, [open], or some other state where the browser already knows the previous computed style, a normal transition is enough.
Use a regular transition for state changes like this:
.button {
background: #111827;
transition: background-color 0.2s ease;
}
.button:hover {
background: #1f2937;
}
Use @starting-style only when the browser would otherwise have no “before” value to animate from.
Animating from display: none
Toggling display: none normally prevents transitions because the property switches discretely (instantly). To animate across the display: none boundary, you need two things:
display Xs allow-discretein thetransitionshorthand — enables transition-behavior: allow-discrete for the display property@starting-stylefor the entry starting state
.drawer {
display: block;
opacity: 1;
transform: translateX(0);
transition:
opacity 0.3s ease,
transform 0.3s ease,
display 0.3s allow-discrete; /* enables discrete display toggling */
}
.drawer.is-closed {
display: none;
opacity: 0;
transform: translateX(-100%);
}
/* Entry animation — fires when .is-closed is removed */
@starting-style {
.drawer {
opacity: 0;
transform: translateX(-100%);
}
}
The allow-discrete keyword is equivalent to transition-behavior: allow-discrete applied to a specific property in the transition shorthand.
Popover API Animations
The Popover API moves elements into and out of the CSS top layer. Animating popover entry and exit requires three components:
@starting-style— entry start values when the popover opensdisplay allow-discrete— allows the display property to transitionoverlay allow-discrete— keeps the element in the top layer during the closing animation
/* Exit / closed state */
[popover] {
opacity: 0;
transform: scale(0.95);
transition:
opacity 0.25s ease,
transform 0.25s ease,
display 0.25s allow-discrete,
overlay 0.25s allow-discrete;
}
/* Open state */
[popover]:popover-open {
opacity: 1;
transform: scale(1);
}
/* Entry start state */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
Why overlay allow-discrete?
Without overlay allow-discrete, the popover exits the top layer immediately when it closes, making it invisible before the closing animation can run. overlay allow-discrete delays the top-layer exit until after the transition completes.
Dialog Element Animations
Dialog elements also use the top layer when opened via showModal(). The pattern is identical to popover animations, using dialog[open] as the open-state selector:
dialog {
opacity: 0;
transform: scale(0.95) translateY(8px);
transition:
opacity 0.2s ease,
transform 0.2s ease,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: scale(1) translateY(0);
}
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.95) translateY(8px);
}
}
Animating the Dialog Backdrop
The ::backdrop pseudo-element can also use @starting-style:
dialog::backdrop {
background-color: rgb(0 0 0 / 0.5);
transition:
background-color 0.2s ease,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
@starting-style {
dialog::backdrop {
background-color: rgb(0 0 0 / 0);
}
}
The overlay Property
The overlay property is a special CSS property that controls whether an element participates in the CSS top layer. It is automatically applied by the browser to top-layer elements (popovers, dialogs, fullscreen elements). Developers cannot set it directly — only allow-discrete can be applied to delay its removal.
| Feature | display allow-discrete | overlay allow-discrete |
|---|---|---|
| What it does | Enables transitions across display: none | Delays top-layer exit until transition ends |
| Required for | display: none animations | Popover/dialog closing animations |
| Safari support | 17.5+ | Not yet |
@starting-style vs. CSS Animations
| Feature | @starting-style | CSS @keyframes |
|---|---|---|
| Trigger | Element appearing | Animation property set |
| Syntax | @starting-style { selector { } } | @keyframes name { from { } to { } } |
| Easing | Transition easing | Animation timing |
| Looping | One-time entry | Configurable |
| display: none | With allow-discrete | Requires JS hack |
| Complexity | Simple | More verbose |
@starting-style is best for entry transitions — a single animation when an element first appears. @keyframes is better for repeated, looping, or complex multi-step animations.
Browser Support
| Browser | @starting-style | display allow-discrete | overlay allow-discrete |
|---|---|---|---|
| Chrome | 117+ | 117+ | 117+ |
| Firefox | 129+ | 129+ | 129+ |
| Safari | 17.5+ | 17.5+ | Not yet |
| Edge | 117+ | 117+ | 117+ |
| Opera | 103+ | 103+ | 103+ |
Global coverage: approximately 85–90% of modern browsers as of early 2026. Safari does not yet support overlay allow-discrete — popover/dialog closing animations fall back to instant exit in Safari.
Detecting Support
/* Detect transition-behavior: allow-discrete support */
@supports (transition-behavior: allow-discrete) {
/* @starting-style also supported in these browsers */
.element {
transition: opacity 0.3s ease;
}
@starting-style {
.element {
opacity: 0;
}
}
}
/* Detect @starting-style specifically */
@supports at-rule(@starting-style) {
@starting-style {
.element { opacity: 0; }
}
}
Frequently Asked Questions
Does @starting-style fire on every render or just the first?
@starting-style fires only when the element enters a new state for the first time: when it is first rendered (inserted into the DOM), or when it transitions from display: none to display: block. It does not re-fire if the element is already visible and a different property changes.
Can @starting-style be used without a CSS transition?
No. @starting-style only works when a CSS transition is applied to the element. Without a transition, the browser applies the starting styles and immediately jumps to the computed styles with no animation.
What is the difference between @starting-style and animation-fill-mode: backwards?
animation-fill-mode: backwards applies the from-keyframe values before an animation begins, preventing a flash of the final styles. @starting-style works with CSS transitions (not animations) and specifically targets the “element first appears” case where there is no previous computed style.
Does @starting-style work with CSS custom properties?
Yes. You can animate CSS custom properties (variables) using @starting-style as long as @property is used to register the property as animatable (CSS custom properties are not animatable by default).
Can I nest @starting-style inside another at-rule?
Yes. @starting-style can be nested inside @layer, @supports, @media, and other at-rules. It can also appear inside a regular style rule using CSS nesting.
Does @starting-style work with JavaScript-inserted elements?
Yes. @starting-style fires for elements inserted into the DOM via element.appendChild() or innerHTML, as long as the element has a CSS transition and matching @starting-style rules.
Related Tools
- HTML Popover API Reference — Complete reference for the Popover API with popover types, anchoring, and accessibility
- HTML Dialog Element Reference — The HTML dialog element, showModal(), and dialog events
- CSS View Transitions Reference — Cross-document and same-document view transition animations
- CSS @scope Reference & Generator — Generate @scope CSS with donut scope and proximity specificity examples