# Use @starting-style for Entry Animations

**SHOULD** · **ID:** `animations-emil-starting-style` · **Category:** animations
**Source:** [Emil Kowalski](https://emilkowalski.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-emil-starting-style

> SHOULD: Animate first-render entrances with `@starting-style` instead of a `useEffect` mounted-flag plus double `requestAnimationFrame` — it degrades to an instant appearance where unsupported.

Animate elements in on first render with @starting-style instead of a JS mounted-flag hack

A CSS transition needs a previous value to interpolate from, and a freshly-inserted element has none — so declaring a transition alone makes the element pop in. `@starting-style` supplies the "before it existed" style, letting the browser animate the very first frame. That removes the classic React workaround (mount flag + double requestAnimationFrame), which costs an extra render and produces intermittent "sometimes it does not animate" bugs when the browser paints before the flag flips. Where unsupported it degrades to an instant appearance.

## Rule snippet

```tsx
.popover { opacity: 1; transition: opacity 200ms var(--ease-out); }
@starting-style { .popover { opacity: 0; } }
```

## Bad — do not do this

`animations-emil-starting-style-bad`

```tsx
import { useState } from 'react';

/**
 * Bad: a transition is declared, but there is no style to interpolate *from* on
 * the very first rendered frame — so the toast simply pops into existence.
 * The usual workaround is a JS mounted-flag plus a double requestAnimationFrame,
 * which means an extra render and a class of "sometimes it doesn't animate" bugs.
 */
export function EmilStartingStyleBad() {
  const [shown, setShown] = useState(false);

  return (
    <div className="w-full max-w-sm space-y-4">
      <button
        onClick={() => setShown((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {shown ? 'Hide toast' : 'Show toast'}
      </button>

      <div className="h-20 rounded-lg bg-muted p-3">
        {shown && (
          <div
            className="rounded-md border border-border bg-card p-3 text-sm text-card-foreground"
            style={{
              // Nothing to animate from: the element's first frame IS the end state.
              transition:
                'transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1)',
              transform: 'translateY(0)',
              opacity: 1,
            }}
          >
            Changes saved
          </div>
        )}
      </div>

      <p className="text-xs text-error">
        The toast pops in — a transition cannot interpolate from "not rendered yet"
      </p>
    </div>
  );
}
```

## Good — do this

`animations-emil-starting-style-good`

```tsx
import { useState } from 'react';

/**
 * Good: `@starting-style` gives the browser the "before it existed" style, so a
 * plain CSS transition animates the entry. No mounted flag, no double rAF, no
 * extra render — and it degrades to an instant appearance where unsupported.
 */
export function EmilStartingStyleGood() {
  const [shown, setShown] = useState(false);

  return (
    <div className="w-full max-w-sm space-y-4">
      <style>{`
        .starting-style-toast {
          opacity: 1;
          transform: translateY(0);
          transition:
            transform 300ms cubic-bezier(0.23, 1, 0.32, 1),
            opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
        }
        @starting-style {
          .starting-style-toast {
            opacity: 0;
            transform: translateY(-16px);
          }
        }
        @media (prefers-reduced-motion: reduce) {
          .starting-style-toast {
            transform: none;
            transition: opacity 150ms linear;
          }
        }
      `}</style>

      <button
        onClick={() => setShown((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {shown ? 'Hide toast' : 'Show toast'}
      </button>

      <div className="h-20 rounded-lg bg-muted p-3">
        {shown && (
          <div className="starting-style-toast rounded-md border border-border bg-card p-3 text-sm text-card-foreground">
            Changes saved
          </div>
        )}
      </div>

      <p className="text-xs text-success">
        @starting-style supplies the entry state — the toast slides and fades in with zero JS
      </p>
    </div>
  );
}
```

## References

- [Emil Kowalski — review-animations STANDARDS](https://github.com/emilkowalski/skills/tree/main/skills/review-animations)
- [MDN — @starting-style](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@starting-style)
