# Stagger Group Entrances

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

> SHOULD: Stagger entrance animations for groups of items by 30–80ms. A whole list animating at once reads as a single flash.

Entrance animations for a group of items should stagger 30–80ms, not fire all at once

When a whole list appears at once, the motion reads as a single flash. A small 30–80ms stagger guides the eye down the list and makes the group feel orchestrated rather than abrupt.

## Bad — do not do this

`animations-emil-stagger-bad`

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

const items = ['Inbox', 'Drafts', 'Sent', 'Archive', 'Spam'];

export function EmilStaggerBad() {
  const [loaded, setLoaded] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setLoaded((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        {loaded ? 'Reset' : 'Load list'}
      </button>
      <ul className="space-y-2 min-h-[12rem]">
        {loaded &&
          items.map((item) => (
            <li
              key={item}
              className="p-2 rounded-md bg-muted text-sm"
              style={{ animation: 'emilFadeSlide 300ms cubic-bezier(0.16, 1, 0.3, 1) both' }}
            >
              {item}
            </li>
          ))}
      </ul>
      <style>{`
        @keyframes emilFadeSlide {
          from { opacity: 0; transform: translateY(8px); }
          to { opacity: 1; transform: translateY(0); }
        }
      `}</style>
      <p className="text-xs text-destructive">
        Every item animates at once; the entrance reads as a single flash
      </p>
    </div>
  );
}
```

## Good — do this

`animations-emil-stagger-good`

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

const items = ['Inbox', 'Drafts', 'Sent', 'Archive', 'Spam'];

export function EmilStaggerGood() {
  const [loaded, setLoaded] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setLoaded((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        {loaded ? 'Reset' : 'Load list'}
      </button>
      <ul className="space-y-2 min-h-[12rem]">
        {loaded &&
          items.map((item, i) => (
            <li
              key={item}
              className="p-2 rounded-md bg-muted text-sm"
              style={{
                animation: 'emilFadeSlide 300ms cubic-bezier(0.16, 1, 0.3, 1) both',
                animationDelay: `${i * 50}ms`,
              }}
            >
              {item}
            </li>
          ))}
      </ul>
      <style>{`
        @keyframes emilFadeSlide {
          from { opacity: 0; transform: translateY(8px); }
          to { opacity: 1; transform: translateY(0); }
        }
      `}</style>
      <p className="text-xs text-success">
        A 50ms stagger guides the eye down the list and feels orchestrated
      </p>
    </div>
  );
}
```

## References

- [Emil Kowalski — review-animations](https://github.com/emilkowalski/skills/tree/main/skills/review-animations)
