Never animate from scale(0)

Enter from scale(0.95) and opacity: 0, never from zero.

Emil Kowalski@emilkowalski
Animating from `scale(0)` usually feels "off". Try animating from a higher initial scale instead (0.9+). It helps it feel more gentle and elegant. Here's the difference between initial `scale(0)` and `scale(0.93)`:
Aug 11, 2025View on X ↗

From emil-design-eng; demo 1 rebuilds his Options popover.

The Options popover

Open both. Only the starting scale differs.

scale(0)Bad
scale(0) → 1opacity 0 → 1
scale(0.93)Good
scale(0.93) → 1opacity 0 → 1

.menu {
  transform-origin: top center;
  opacity: 0;
  transform: scale(0.93);
  visibility: hidden;
  transition: opacity 200ms var(--ease-out),
              transform 200ms var(--ease-out),
              visibility 0s linear 200ms;
}
.menu[data-open="true"] {
  opacity: 1;
  transform: none;
  visibility: visible;
  transition-delay: 0s;
}

Find the floor

Pick a start scale, then show the toast.

scale(0.93) → 1Good
Event createdTomorrow at 9:00 AM

First frames, 60 fps

scale(0.93) · opacity 0
toast.animate(
  [
    { transform: "scale(0.93)", opacity: 0 },
    { transform: "scale(1)", opacity: 1 },
  ],
  { duration: 200, easing: "cubic-bezier(0.23, 1, 0.32, 1)" },
);

Type a message

Send a message to both threads.

scale(0), no fadeBad

Lunch tomorrow?

transform: scale(0)
scale(0.95) + opacityGood

Lunch tomorrow?

transform: scale(0.95); opacity: 0

.entering { transform: scale(0.95); opacity: 0; }
.bubble { transform-origin: bottom right; }

Why it matters, pros and cons

Why it matters

From scale(0) the first frames are a dot. From 0.93 it is itself at once.

Pros

  • Readable from the first frame.
  • Less travel, so it feels faster.
  • Transform and opacity stay on the compositor.

Cons

  • Below 0.9 it reads as a zoom again.
  • Scale without the fade is a one-frame jump.
  • Ripples and badge dots may truly grow from 0.