Using @starting-style for CSS-Only Element Entry Animations: A Complete Guide
@starting-style is a CSS at-rule that defines an element's initial visual state before the first paint, enabling smooth entrance animations without JavaScript.
The @starting-style rule lets you declare starting values for properties that differ from the element's default styles. Once the browser renders the first frame, normal styles apply and CSS transitions animate the change to the final state. This technique—documented extensively in the emilkowalski/skills repository—eliminates the need for useEffect hooks, data-mounted attributes, or any JavaScript timing logic for static entry animations.
How @starting-style Works
The @starting-style block lives inside a CSS rule and contains property values that exist only for that crucial first frame.
/* From skills/review-animations/STANDARDS.md */
.toast {
/* Final state */
opacity: 1;
transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
/* Initial state before first paint */
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
The element renders at opacity: 0 with translateY(100%), then immediately transitions to the visible, centered position. The browser handles the timing automatically.
Why Prefer @starting-style Over JavaScript
According to skills/review-animations/STANDARDS.md (line 89) and SKILL.md (line 73), this approach offers three concrete advantages:
- Zero-JS overhead – The browser computes start states during layout, avoiding extra render passes from mounting logic
- Compositor-thread execution – Animations on
opacityandtransformrun on the GPU, keeping the main thread free - Instant interruptibility – CSS transitions can be overridden immediately by new style rules, unlike animation libraries that queue state changes
Browser Fallback Strategy
When @starting-style isn't supported—older Safari and Chrome versions—the repository recommends the classic data-mounted pattern documented at line 99 of STANDARDS.md:
/* Fallback for unsupported browsers */
.toast[data-mounted] {
opacity: 1;
transform: translateY(0);
}
// Minimal React bridge for the fallback
useEffect(() => {
const el = ref.current;
if (el) el.dataset.mounted = '';
}, []);
The JavaScript runs only when necessary. Feature-detect @starting-style support and conditionally apply the fallback.
Practical Implementation Patterns
Toast Notification Entry
/* skills/review-animations/STANDARDS.md */
.toast {
opacity: 1;
transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
The toast slides up from the bottom while fading in—no pop-in, no layout shift.
Staggered List Items
.list-item {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease, transform 300ms ease;
transition-delay: var(--delay);
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
Set --delay per item (0ms, 50ms, 100ms) for cascading entrance effects. All timing lives in CSS.
Reduced Motion Support
@media (prefers-reduced-motion: reduce) {
.toast {
transition: none;
@starting-style {
opacity: 1;
transform: translateY(0);
}
}
}
Users who prefer reduced motion see the final state instantly without animation.
Performance Characteristics
The skills/improve-animations/AUDIT.md file identifies @starting-style as the "entry without JS" pattern for auditing purposes. Key metrics:
| Property | Impact |
|---|---|
opacity |
Composite-only, minimal cost |
transform |
GPU-accelerated, no layout recalculation |
@starting-style overhead |
Single frame, discarded after paint |
Avoid animating width, height, margin, or top/left—these trigger layout and paint, negating the performance benefits.
File Reference Guide
These source files contain implementation details and architectural decisions:
skills/review-animations/STANDARDS.md– Core documentation, fallback strategy (line 99), and canonical examplesskills/review-animations/SKILL.md– High-level guidance on preferring CSS transitions over JavaScriptskills/improve-animations/AUDIT.md– Checklist reference for the entry-without-JS patternskills/emil-design-eng/SKILL.md– Concrete component example in the "Animate enter states with @starting-style" section
Summary
@starting-styledefines pre-paint property values for CSS-only entrance animations- No JavaScript required for static entry effects—transitions handle the interpolation
- Fallback to
data-mountedwhen browser support is missing - Animate only
opacityandtransformto stay on the compositor thread - Wrap in
prefers-reduced-motionto respect accessibility preferences
Frequently Asked Questions
Which browsers support @starting-style?
Chrome 117+, Edge 117+, and Safari 17.4+ support @starting-style. Firefox has not yet implemented the feature. The emilkowalski/skills repository recommends feature detection rather than browser sniffing, applying the data-mounted fallback only when the at-rule fails to parse.
Can @starting-style animate height or width?
Technically yes, but the repository explicitly discourages this. Animating dimensional properties triggers layout recalculation and harms performance. Stick to opacity and transform per the standards in STANDARDS.md—these properties can be promoted to GPU layers without main-thread involvement.
How does @starting-style differ from CSS @keyframes?
@keyframes define explicit animation sequences that run for a set duration. @starting-style declares a single initial state that immediately transitions to normal styles. Use @starting-style for simple entry effects; reserve @keyframes for complex multi-stage animations or looping effects.
What happens if I nest @starting-style inside a media query?
The rule respects its container's conditions. Nesting inside @media (prefers-reduced-motion: reduce) is the recommended pattern for accessibility—the starting state matches the final state, eliminating motion while maintaining visual content.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →