# Proper Animation Timing

**NEVER** · **ID:** `animations-ibelick-timing` · **Category:** animations
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-ibelick-timing

> NEVER: Exceed 200ms for interaction feedback. Use ease-out on entrance, ease-in on exit. Never introduce custom easing curves unless explicitly requested.

Use ease-out for entrances and keep interaction feedback under 200ms

Two separate upstream rules, quoted together because they answer the same question: how should a small interaction feel. ease-out on entrance puts the movement in the first frames — the ones the user is actually watching — and lets it settle, which is why a 200ms ease-out reads as faster than a 200ms ease-in even though the stopwatch says otherwise (animations-emil-no-ease-in makes the same case at length). The 200ms ceiling is specifically for INTERACTION FEEDBACK — a press, a hover, a toggle, the response to something the user just did — not for every animation on the page: a drawer or a modal moves a large surface and legitimately runs longer (animations-emil-duration-budget budgets 200–500ms for those). Upstream says nothing about the curve for exits; do not infer ease-in from its absence.

## Bad — do not do this

`animations-ibelick-timing-bad`

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

export function IbelickTimingBad() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setIsVisible(!isVisible)}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-lg transition-colors duration-700 ease-in hover:bg-primary/90"
      >
        Toggle Panel
      </button>
      {isVisible && (
        <div
          className="p-4 bg-muted rounded-lg"
          style={{
            animation: 'slowLinearIn 800ms linear',
          }}
        >
          <p>Panel content with slow linear animation</p>
        </div>
      )}
      <style>{`
        @keyframes slowLinearIn {
          from { opacity: 0; transform: translateY(20px); }
          to { opacity: 1; transform: translateY(0); }
        }
      `}</style>
      <p className="text-xs text-destructive">
        800ms linear animation feels sluggish and robotic
      </p>
    </div>
  );
}
```

## Good — do this

`animations-ibelick-timing-good`

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

export function IbelickTimingGood() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setIsVisible(!isVisible)}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-lg transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Toggle Panel
      </button>
      {isVisible && (
        <div
          className="p-4 bg-muted rounded-lg"
          style={{
            animation: 'quickEaseOut 200ms cubic-bezier(0, 0, 0.2, 1)',
          }}
        >
          <p>Panel content with snappy ease-out animation</p>
        </div>
      )}
      <style>{`
        @keyframes quickEaseOut {
          from { opacity: 0; transform: translateY(10px); }
          to { opacity: 1; transform: translateY(0); }
        }
      `}</style>
      <p className="text-xs text-success">
        200ms ease-out feels responsive and natural
      </p>
    </div>
  );
}
```

## References

- [ibelick — baseline-ui SKILL.md](https://github.com/ibelick/ui-skills/blob/main/skills/baseline-ui/SKILL.md)
- [Material Design Motion](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs)
