Using @starting-style for CSS Entry Animations Without JavaScript
@starting-style is a CSS at-rule that defines an element's initial visual state before the browser paints the first frame, enabling JavaScript-free entrance animations.
The @starting-style at-rule lets you create smooth, performant entry animations entirely in CSS. According to the emilkowalski/skills repository (the Instagit skillset), this technique eliminates the need for React useEffect hooks or data-mounted attributes while keeping animations on the GPU compositor thread.
What @starting-style Does and How It Works
@starting-style declares properties that apply only during the first paint cycle. After that frame renders, the element reverts to its normal styles, triggering CSS transitions to animate between the two states.
This architecture provides three key advantages:
- Zero JavaScript overhead — the browser computes start states during layout, avoiding extra render passes from mounting logic
- GPU acceleration — when combined with
opacityandtransformtransitions, animations run entirely on the compositor thread - Instant reversibility — CSS transitions can be overridden immediately by new style rules, unlike keyframe animations
The technique is documented in [skills/review-animations/STANDARDS.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L89) and promoted throughout the animation guides as the preferred approach for static entry animations.
Basic Toast Entry Animation Example
Here's the canonical pattern from STANDARDS.md:
/* File: skills/review-animations/STANDARDS.md */
.toast {
/* Final state after entry */
opacity: 1;
transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
/* Initial state before the first paint */
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
The toast starts invisible and off-screen (translateY(100%)). Once painting completes, the transition automatically animates to the final visible position. No state management, no refs, no effects required.
Browser Fallback Strategy
For older Safari and Chrome versions lacking @starting-style support, the repository recommends the classic data-mounted pattern at line 99 of STANDARDS.md:
/* Fallback when @starting-style isn't supported */
.toast[data-mounted] {
opacity: 1;
transform: translateY(0);
}
// In your React component
useEffect(() => {
const el = ref.current;
if (el) el.dataset.mounted = '';
}, []);
The JavaScript fallback applies the same visual result but requires an extra render pass. Feature detection via @supports (@starting-style {}) lets you serve the optimal path to each browser.
Respecting Reduced Motion Preferences
Accessibility considerations appear throughout the skillset documentation. Wrap @starting-style in a media query to honor user preferences:
@media (prefers-reduced-motion: reduce) {
.toast {
transition: none;
@starting-style {
opacity: 1;
transform: translateY(0);
}
}
}
This disables the transition while ensuring the element renders immediately in its final state—no disorienting animation, no broken layout.
Staggered List Entry Pattern
For cascading entrances, combine @starting-style with CSS custom properties:
.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 (e.g., calc(var(--index) * 50ms)) to create choreographed reveals without JavaScript timers or batch updates.
Performance and Architectural Benefits
| Concern | @starting-style Approach |
Traditional JS Approach |
|---|---|---|
| Render passes | Single layout + paint | Multiple React renders |
| Thread usage | Compositor-only with transform/opacity |
Main thread for state updates |
| Bundle impact | Zero bytes of JS | Hooks, refs, and effect logic |
| Cancellation | Instant via new style rules | Requires cleanup functions |
The [skills/review-animations/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md#L73) file explicitly recommends CSS transitions over JavaScript for static entry animations precisely because of these characteristics.
Common Use Cases in the Codebase
The emilkowalski/skills repository identifies these patterns as ideal candidates for @starting-style:
- Toast notifications — slide up from bottom while fading in
- Modal dialogs — scale from 95% with opacity fade
- Dropdown menus — expand from origin point
- List items — staggered entrance on mount
Each case shares the same requirements: a predictable initial state, a single transition target, and no need for dynamic interruption during the animation.
Key Files and References
| File | Purpose |
|---|---|
skills/review-animations/STANDARDS.md |
Core documentation with implementation examples and fallback strategy |
skills/review-animations/SKILL.md |
High-level guidance on preferring CSS transitions for entry animations |
skills/improve-animations/AUDIT.md |
Checklist entry for "entry without JS" pattern |
skills/emil-design-eng/SKILL.md |
Concrete component example under "Animate enter states with @starting-style" |
Summary
@starting-styledefines pre-paint styles that enable CSS-only entrance animations- No JavaScript required — eliminate
useEffect,useState, anddata-mountedpatterns for static entries - GPU-optimized when paired with
opacityandtransformtransitions - Graceful degradation via
data-mountedfallback for unsupported browsers - Accessible by default through
prefers-reduced-motionmedia query integration - Best for toast notifications, modals, dropdowns, and staggered list items
Frequently Asked Questions
Which browsers support @starting-style?
Chrome 116+ and Safari 17.4+ support @starting-style. For older versions, implement the data-mounted fallback documented in STANDARDS.md line 99. Use @supports (@starting-style {}) for feature detection.
Can I animate height or width with @starting-style?
Technically yes, but the repository discourages it. Animating layout properties forces main-thread recalculation and subverts the performance benefits. Stick to opacity and transform for compositor-thread animation.
How does @starting-style differ from CSS @keyframes?
@keyframes define an animation sequence that plays once or loops. @starting-style sets a single initial state that transitions to normal styles. The latter integrates naturally with CSS transitions and requires no animation duration declaration in the keyframe itself.
What happens if I wrap @starting-style in a media query?
The rule respects query conditions normally. In prefers-reduced-motion: reduce, setting the starting style equal to the final style prevents any visible animation while maintaining the same structural approach.
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 →