CSS @starting-style: Entry Animations Without JavaScript
Animate elements appearing from display: none using @starting-style—pure CSS entry transitions without JavaScript timing hacks.
For years, animating elements appearing from display: none required JavaScript.
CSS transitions don't trigger on initial render—the element snaps to its final state instantly.
@starting-style fixes this: define where transitions should start from, and CSS
handles entry animations natively.
The Problem: Why Transitions Fail on Entry
CSS transitions require a "before" and "after" state. When an element first appears
(initial render, or switching from display: none), there's no "before"—the
element didn't exist. Browsers skip the transition entirely:
/* ❌ This transition never fires on initial render */
.modal {
opacity: 1;
transition: opacity 0.3s;
}
.modal.hidden {
display: none;
opacity: 0;
}
The classic workaround: JavaScript to add a class after a requestAnimationFrame,
forcing two style recalcs. Fragile and verbose.
The Solution: @starting-style
@starting-style defines the initial values for CSS properties when an element
first renders. The browser uses these as the "from" state, enabling true entry transitions:
/* ✅ Entry animation works */
.modal {
opacity: 1;
transition: opacity 0.3s;
}
@starting-style {
.modal {
opacity: 0;
}
}
On first render, the modal starts at opacity: 0 (from @starting-style)
and transitions to opacity: 1. No JavaScript timing hacks needed.
Nested Syntax
@starting-style can nest inside rulesets, which is cleaner when the selector is complex:
.modal[open] {
opacity: 1;
transform: scale(1);
transition: opacity 0.3s, transform 0.3s;
@starting-style {
opacity: 0;
transform: scale(0.95);
}
}Animating display: none
To animate elements toggling display, combine @starting-style with
transition-behavior: allow-discrete. This tells the browser to include
display in the transition timeline:
.dropdown {
display: block;
opacity: 1;
transition: opacity 0.3s, display 0.3s;
transition-behavior: allow-discrete;
@starting-style {
opacity: 0;
}
}
.dropdown.hidden {
display: none;
opacity: 0;
}
Entry: display switches to block immediately,
then opacity transitions from 0 to 1.
Exit: opacity transitions to 0, then display
switches to none at 100% progress.
Popovers and Dialogs
@starting-style shines with the Popover API and <dialog>
elements, which manage display internally:
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
transition: opacity 0.25s, transform 0.25s, display 0.25s;
transition-behavior: allow-discrete;
@starting-style {
opacity: 0;
transform: translateY(-10px);
}
}
/* Exit transition requires explicit hidden state */
[popover]:not(:popover-open) {
opacity: 0;
transform: translateY(-10px);
}
The :popover-open pseudo-class triggers when the popover enters the top layer.
Combined with @starting-style, you get smooth entry animations without
managing visibility state in JavaScript.
The Three-State Model
When using @starting-style, there are three distinct style states to consider:
- Starting state: From
@starting-style—used only on first render - Active state: The element's visible/active styles
- Default state: The element's hidden/inactive styles
/* Default: hidden */
.toast {
display: none;
opacity: 0;
transform: translateY(100%);
transition: opacity 0.3s, transform 0.3s, display 0.3s;
transition-behavior: allow-discrete;
}
/* Active: visible */
.toast.show {
display: block;
opacity: 1;
transform: translateY(0);
}
/* Starting: entry animation origin */
@starting-style {
.toast.show {
opacity: 0;
transform: translateY(100%);
}
}
Entry: @starting-style → active state.
Exit: active state → default state (not @starting-style—it's only for entry).
Edge Cases and Gotchas
Specificity Matters
@starting-style has the same specificity as the selector inside it.
Place it after the main ruleset to ensure it's applied:
/* ❌ Wrong order: starting-style gets overridden */
@starting-style {
.modal { opacity: 0; }
}
.modal { opacity: 1; }
/* ✅ Correct: starting-style comes after */
.modal { opacity: 1; }
@starting-style {
.modal { opacity: 0; }
}Only Works for CSS Transitions
@starting-style has no effect on CSS @keyframes animations.
For animations, use animation-fill-mode: backwards or define the
start state in the 0%/from keyframe.
DOM Insertion vs Class Toggle
@starting-style triggers when an element first renders matching the selector.
If you toggle a class on an already-rendered element, it works. But removing and
re-adding the same class in the same frame won't retrigger—the browser optimizes
it away.
Real-World Example: Toast Notifications
<div class="toast" id="toast">
Saved successfully!
</div>
<button onclick="showToast()">Save</button>
<script>
function showToast() {
const toast = document.getElementById('toast');
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
</script>.toast {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 1rem 1.5rem;
background: #333;
color: white;
border-radius: 0.5rem;
/* Hidden state */
display: none;
opacity: 0;
transform: translateX(100%);
/* Transition setup */
transition: opacity 0.3s ease-out,
transform 0.3s ease-out,
display 0.3s;
transition-behavior: allow-discrete;
}
.toast.show {
display: block;
opacity: 1;
transform: translateX(0);
}
@starting-style {
.toast.show {
opacity: 0;
transform: translateX(100%);
}
}Browser Support
@starting-style is supported in Chrome 117+, Edge 117+, Safari 17.4+, and
Firefox 129+. For older browsers, fall back to JavaScript-based animations or accept
instant transitions:
@supports (transition-behavior: allow-discrete) {
/* Modern entry animations */
.modal {
@starting-style {
opacity: 0;
}
}
}
@supports not (transition-behavior: allow-discrete) {
/* Fallback: no entry animation, just instant display */
}Key Takeaways
@starting-styledefines the "from" state for entry transitions—no JavaScript needed- Combine with
transition-behavior: allow-discreteto animatedisplaychanges - Perfect for popovers, dialogs, modals, and toasts
- Remember the three-state model: starting → active ↔ default
- Place
@starting-styleafter main rulesets for correct specificity
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement