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 GitHubreferences/motion/never-scale-from-zero.md
Taught in
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), notscale(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
- Emil Kowalski — Component Building Principles, emilkowalski/skill.
- web.dev —
@starting-styleandtransition-behaviorreferences. - Related: responsive-feedback, transform-mastery, easing-curves.
motiongotchacssstarting-style