Skip to content
ExamsLibraryFor agentsConnect your agent
motionMotion·25/33

Never animate from scale(0)

Never animate from scale(0). Use scale(0.95) + opacity, or @starting-style for modern CSS entry.

Raw markdown On GitHub

The single most common amateur mistake in entry animations is starting at scale(0) and animating to scale(1). It looks like a balloon being inflated. UI is not a balloon.

Why scale(0) is wrong

  • The element is invisible during the first ~30ms of the transition, then suddenly becomes visible at scale(0.05) or so.
  • The visual mass appears from a single point, which reads as theatrical and toy-like.
  • Tiny scale values render to subpixel positions, causing blur and shimmer on the first frames.

The right pattern: 0.95 + opacity

.modal {
  opacity: 0;
  transform: scale(0.95);
  transition:
    opacity 200ms var(--ease-out-quart),
    transform 200ms var(--ease-out-quart);
}
.modal[data-state="open"] {
  opacity: 1;
  transform: scale(1);
}
  • Start at scale(0.95), not scale(0). The 5% size difference is enough to read as motion.
  • Pair with opacity: 0 → 1. The opacity does most of the visibility work.
  • Both transitions on the same duration and curve — they should feel like a single move.

For more theatrical entries (rare moments, see delight-impact-curve), scale(0.85) or even scale(0.5) can work if paired with a spring curve. But scale(0) is almost never right.

@starting-style — the modern CSS way

Chrome 117+ and Safari 17.5+ support @starting-style, which lets you define the "before" state for an element that's just been added to the DOM:

.modal {
  opacity: 1;
  transform: scale(1);
  transition:
    opacity 200ms,
    transform 200ms,
    display 200ms;
}
@starting-style {
  .modal {
    opacity: 0;
    transform: scale(0.95);
  }
}

No data-state toggle, no JavaScript. The element animates in the moment it's added to the DOM (or becomes display: block from display: none).

For the exit in the same model, use transition-behavior: allow-discrete and animate the display property — modern browsers support this.

Fallback for browsers without @starting-style

Use a data-mounted attribute set in JS one frame after mount:

const [mounted, setMounted] = useState(false);
useEffect(() => {
  requestAnimationFrame(() => setMounted(true));
}, []);
 
return <div data-mounted={mounted} className="modal">…</div>;
.modal {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 200ms var(--ease-out-quart), transform 200ms var(--ease-out-quart); /* never `all` */
}
.modal[data-mounted="true"] {
  opacity: 1;
  transform: scale(1);
}

This is the React pattern most production apps use today.

When to apply

Every entrance animation. Modals, popovers, tooltips, toasts, dropdowns, drawers — anywhere an element appears on screen.

Gotcha

@starting-style only fires on the first render of an element. For elements that mount and unmount repeatedly (e.g. dropdowns opened multiple times), the pattern still works if you display: none between opens and rely on the transition-behavior trick. But on rapid open/close, you may see flicker — at that point switch to the data-mounted fallback or use a library like Radix that handles this for you.

Sources

motiongotchacssstarting-style