How to Animate Enter and Exit States with `@starting-style` in CSS

The @starting-style CSS at-rule lets you define an element's initial visual state before the first paint, enabling entry animations without JavaScript by declaring starting properties that automatically transition to the element's normal styles.

The emilkowalski/skills repository documents how to leverage @starting-style in CSS to create smooth enter and exit animations while eliminating the need for React mount flags or useEffect tricks. This approach reduces JavaScript bundle size and guarantees animations run even when JavaScript is disabled.

What is @starting-style?

@starting-style is a modern CSS at-rule that specifies the initial visual state of an element before the browser applies its normal styles. When an element renders, the browser instantly swaps the starting style for the final style, triggering any declared CSS transitions to create fluid entry animations.

How the At-Rule Works

The rule is parsed as a nested block inside a selector. Properties declared within @starting-style apply only during the first paint cycle. Immediately after, the browser applies the element's standard computed styles, causing the transition to animate from the starting values to the final values. This mechanism is documented in skills/review-animations/STANDARDS.md within the emilkowalski/skills repository.

Creating Entry Animations

To animate an element as it enters the DOM, define both the final state and the starting state within the same selector. The browser automatically handles the transition between these states on mount.

.toast {
  /* Final state after mount */
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease, transform 400ms ease;

  /* Initial state – applied only on first paint */
  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }
}

In this example from skills/emil-design-eng/SKILL.md, the toast starts fully transparent and translated downward. On the first paint, it instantly adopts these starting values, then transitions to the final opacity: 1 and transform: translateY(0) states over 400 milliseconds.

Handling Exit Animations

Since @starting-style only manages entry states, exit animations require a different approach. You implement exits using standard CSS transitions by toggling a class that changes the property values toward their initial states.

/* Exit state triggered by adding a class */
.toast.exit {
  opacity: 0;
  transform: translateY(100%);
}

When the exit class is applied—or when the element is removed from the DOM—the browser transitions the properties back to their initial values, creating a smooth exit animation.

Browser Support and Fallback Strategy

@starting-style is currently supported in Chrome 106+, Edge 106+, and Safari 16.0+. For browsers lacking support, you can fall back to the classic mount-after-render pattern using data attributes.

The Data-Mounted Fallback

When @starting-style is unavailable, manually control the initial state using a data-mounted attribute set via JavaScript:

[data-mounted="true"] .toast {
  opacity: 1;
  transform: translateY(0);
}

[data-mounted="false"] .toast {
  opacity: 0;
  transform: translateY(100%);
}

In React, implement this pattern using useEffect to set the mounted state after the initial render:

function Toast({ children }) {
  const [mounted, setMounted] = React.useState(false);
  
  // Only runs where @starting-style isn't supported
  React.useEffect(() => setMounted(true), []);

  return (
    <div className="toast" data-mounted={mounted}>
      {children}
    </div>
  );
}

This fallback ensures your animations work across all browsers while allowing you to remove the JavaScript overhead in modern environments.

Summary

  • @starting-style defines initial CSS property values that apply only during the first paint, enabling pure CSS entry animations without JavaScript mount flags.
  • Entry animations work by declaring starting properties inside @starting-style and letting the browser transition to the element's normal styles.
  • Exit animations require toggling classes or removing elements to trigger standard CSS transitions, as @starting-style only handles the initial render.
  • Browser support includes Chrome 106+, Edge 106+, and Safari 16.0+, with a data-mounted fallback pattern available for unsupported browsers as documented in skills/review-animations/STANDARDS.md.

Frequently Asked Questions

Does @starting-style work for exit animations?

No, @starting-style only handles entry animations by defining the initial state before the first paint. For exit animations, you must use standard CSS transitions by adding a class that changes properties toward their final hidden state, or by removing the element from the DOM entirely.

Which browsers support @starting-style?

The @starting-style at-rule is supported in Chrome 106 and later, Edge 106 and later, and Safari 16.0 and later. According to the emilkowalski/skills repository standards, you should implement a fallback using data-mounted attributes for browsers that do not yet support this feature.

How do I animate elements that are added to the DOM dynamically?

@starting-style automatically applies to any element when it first renders, regardless of when it is added to the DOM. Simply include the @starting-style block within the element's selector, and the browser applies the starting styles on the next paint cycle, then transitions to the final state.

Can I use @starting-style with display: none?

@starting-style works best with properties that support transitions, such as opacity and transform. Since display is not animatable, you should combine @starting-style with opacity for the visual animation, and control the display property separately using the allow-discrete property or by managing visibility through other means.

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 →