# Easing Fits Subject

**SHOULD** · **ID:** `animations-easing` · **Category:** animations
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-easing

> SHOULD: Choose easing to match the change (size/distance/trigger)

Choose easing functions based on what changes

Pick the curve from what is changing, in this order: elements entering or exiting the screen → ease-out (and, on this reading, ease-in is also permitted for exits, on the argument that a departing element may accelerate away); an element morphing or moving between two on-screen states → ease-in-out; hover and other pointer feedback → ease; anything constant or continuous, like a marquee or a spinner → linear. Then match the magnitude to the distance travelled: a 4px hover nudge and a full-screen sheet should not share a curve. Note the field disagrees about exits — "Never ease-in on UI" (animations-emil-no-ease-in) rejects ease-in outright, on the grounds that it stalls the first ~100ms, which is exactly the window in which the user judges speed. Both entries are kept deliberately: read them together and decide, rather than inheriting one by default.

## Bad — do not do this

`animations-easing-bad`

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

export function EasingBad() {
  const [replay, setReplay] = useState(0);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setReplay((v) => v + 1)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Replay
      </button>
      <div className="relative h-8 w-64 max-w-full rounded-full bg-muted/50">
        <div
          key={replay}
          className="absolute top-1 left-1 size-6 rounded-full bg-primary"
          style={{ animation: 'easeRace 1200ms linear both' }}
        />
      </div>
      <style>{`@keyframes easeRace { from { transform: translateX(0); } to { transform: translateX(224px); } }`}</style>
      <p className="text-xs text-destructive">
        <code>linear</code> — the dot travels at one constant speed the whole way, which reads as robotic
      </p>
    </div>
  );
}
```

## Good — do this

`animations-easing-good`

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

export function EasingGood() {
  const [replay, setReplay] = useState(0);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setReplay((v) => v + 1)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Replay
      </button>
      <div className="relative h-8 w-64 max-w-full rounded-full bg-muted/50">
        <div
          key={replay}
          className="absolute top-1 left-1 size-6 rounded-full bg-primary"
          style={{ animation: 'easeRace 1200ms cubic-bezier(0.16, 1, 0.3, 1) both' }}
        />
      </div>
      <style>{`@keyframes easeRace { from { transform: translateX(0); } to { transform: translateX(224px); } }`}</style>
      <p className="text-xs text-success">
        <code>ease-out</code> — the dot starts fast and eases into place, matching how real objects decelerate and settle
      </p>
    </div>
  );
}
```

## References

- [Easing Functions](https://easings.net/)
- [CSS Easing](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/easing-function)
