How to Use CSS @starting-style for Element Entry Animations

CSS @starting-style is an at-rule that defines the initial state of an element before the browser paints the first frame, enabling pure CSS entry animations without JavaScript mount effects.

The emilkowalski/skills repository documents modern CSS patterns for component animations, including the @starting-style at-rule for declarative entry effects. This technique eliminates the need for React useEffect hooks or data-mounted attributes to trigger animations, reducing bundle size and improving rendering performance by keeping animations on the GPU.

Understanding the @starting-style At-Rule

@starting-style allows you to specify styles that apply only during the initial paint of an element. When the element renders, the browser applies these starting styles first, then immediately transitions to the element's normal styles using any declared transition properties.

According to the repository's animation standards in skills/review-animations/STANDARDS.md (lines 89-96), this pattern replaces the common JavaScript approach of toggling state after mount. The rule ensures that elements appear smoothly without requiring a render-cycle side-effect.

Basic Syntax and Implementation

The canonical pattern defined in skills/emil-design-eng/SKILL.md (lines 19-33) consists of three components:

  • Final state: The normal styles the element maintains after animation
  • @starting-style block: The initial styles applied only for the first paint
  • Transition declaration: How the browser interpolates between states
.toast {
  /* Final state */
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease, transform 400ms ease;
  
  /* Initial state - rendered only on first paint */
  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }
}

When the .toast element enters the DOM, it starts with opacity: 0 and transform: translateY(100%), then immediately transitions to opacity: 1 and transform: translateY(0) over 400 milliseconds.

Practical Examples for UI Components

Toast Entry Animation

The repository defines this pattern for toast notifications that slide up from the bottom while fading in:

/* toast.css */
.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease, transform 400ms ease;

  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }
}
<!-- index.html -->
<div class="toast">New message arrived!</div>

This creates a smooth entry animation without any JavaScript initialization code.

Tooltip Scale Animation

For subtle UI feedback, apply a scale transformation that makes the tooltip appear to grow slightly:

.tooltip {
  opacity: 1;
  transform: scale(0.97);
  transition: opacity 200ms ease-out, transform 200ms ease-out;
  
  @starting-style {
    opacity: 0;
    transform: scale(0.95);
  }
}
<div class="tooltip" role="tooltip">Helpful hint</div>

Staggered List Items

Combine @starting-style with transition-delay to create cascading entry animations:

.item {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 300ms ease-out, transform 300ms ease-out;
  
  @starting-style {
    opacity: 0;
    transform: translateY(20px);
  }
}

/* Stagger delays */
.item:nth-child(1) { transition-delay: 0ms; }
.item:nth-child(2) { transition-delay: 50ms; }
.item:nth-child(3) { transition-delay: 100ms; }
<ul>
  <li class="item">First</li>
  <li class="item">Second</li>
  <li class="item">Third</li>
</ul>

Progressive Enhancement and Fallbacks

Supporting Older Browsers

For browsers that do not yet support @starting-style, the repository recommends keeping the data-mounted technique as a progressive enhancement safety net:

.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease, transform 400ms ease;
  
  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }

  /* Legacy fallback */
  &[data-mounted] {
    opacity: 1;
    transform: translateY(0);
  }
}
// toast.jsx
export default function Toast({ children }) {
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
  return (
    <div className="toast" data-mounted={mounted}>
      {children}
    </div>
  );
}

If the browser supports @starting-style, the animation runs immediately. Otherwise, the data-mounted attribute triggers the same transition after the React effect fires.

Performance Benefits of @starting-style

Using @starting-style for element entry animations provides several advantages over JavaScript-based approaches:

  • No JavaScript overhead: Eliminates useEffect hooks and state management for mount animations, reducing bundle size
  • GPU-only properties: Restricting animations to opacity and transform ensures the browser uses compositor threads without layout or paint recalculations
  • Interruptible transitions: If the element is removed before the transition completes, the browser stops the animation smoothly without visual jumps
  • Declarative reusability: The same CSS class applies to any element, regardless of framework or rendering context

Summary

  • @starting-style defines initial element states that apply only during the first paint, before transitions begin
  • The pattern documented in emilkowalski/skills uses opacity and transform properties to ensure GPU-accelerated, 60fps animations
  • No JavaScript is required for the entry effect, though data-mounted attributes provide fallback support for older browsers
  • This technique applies to common UI patterns including toasts, tooltips, dialogs, and list items
  • Reference implementations appear in skills/review-animations/STANDARDS.md and skills/emil-design-eng/SKILL.md

Frequently Asked Questions

What is CSS @starting-style used for?

@starting-style defines the initial appearance of an element before it receives its normal styles, enabling entry animations without requiring JavaScript to toggle classes after mount. It is particularly useful for components that appear dynamically, such as modals, dropdowns, and notification toasts.

Do I need JavaScript to use @starting-style?

No. The primary advantage of @starting-style is that it eliminates the need for JavaScript mount logic entirely. However, for browsers lacking support, you can implement the data-mounted attribute pattern as a progressive enhancement fallback.

Which properties work best with @starting-style?

opacity and transform are the optimal properties because they trigger GPU acceleration without causing layout or paint operations. Avoid animating width, height, top, left, or margin properties, as these force the browser to recalculate layout and degrade performance.

How do I handle browsers that don't support @starting-style?

Implement a fallback using the data-mounted attribute technique: initialize the element with hidden styles, then use a useEffect hook (in React) or equivalent framework lifecycle method to set data-mounted="true" after the initial render. Browsers that support @starting-style will ignore the fallback, while older browsers will use the attribute-based transition.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →