Why `scale(0)` Creates Unnatural Entrance Animations (And the Correct Alternative)
scale(0) causes unnatural entrance animations because it makes UI elements appear from nothing—violating physical expectations—whereas scale(0.95) combined with opacity: 0 provides a smooth, recognizable starting point that feels realistic.
The emilkowalski/skills repository maintains strict animation standards that explicitly reject scale(0) for entrance effects. These guidelines, documented across multiple skill files, prioritize physical correctness and perceptual continuity in UI motion design.
Why scale(0) Breaks Animation Physics
The core problem with scale(0) is simple: nothing in the physical world materializes from a zero-size state. When an element scales from zero, users experience three distinct issues:
-
No visual cue — Users cannot recognize the element before it fully appears, breaking the expectation that UI elements transition smoothly from a discernible shape.
-
Disrupted spatial continuity — Popovers and dropdowns normally grow outward from their trigger point; scaling from the center severs that anchor, making motion feel detached and artificial.
-
Performance degradation — Scaling from zero forces the browser to recompute layout and paint from an empty box, potentially causing jank on low-end devices.
The repository's animation standards capture this principle bluntly in skills/review-animations/STANDARDS.md#L53:
"Never
scale(0). Start fromscale(0.9–0.97)+opacity: 0. Nothing in the real world appears from nothing."
The Correct Alternative: Near-Zero Scale + Opacity
The recommended pattern uses minimal scale reduction paired with full opacity transition. This preserves the element's shape as a visual reference while still achieving the entrance effect.
Basic Implementation
.enter {
transform: scale(0.95);
opacity: 0;
}
.enter[data-mounted] {
transition: transform 200ms var(--ease-out),
opacity 200ms var(--ease-out);
transform: scale(1);
opacity: 1;
}
As noted in [skills/improve-animations/AUDIT.md](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md), this pattern addresses a high-severity issue in animation audits. The target range of 0.90–0.97 provides enough visual presence to maintain recognition without compromising the entrance effect.
Anchoring Animations to Their Origin Point
For popovers, dropdowns, and tooltips, the transform origin matters as much as the scale value. These elements should appear to grow from their trigger location, not their own center.
.popover {
transform-origin: var(--transform-origin); /* Base UI variable */
transform: scale(0.95);
opacity: 0;
}
.popover[data-mounted] {
transition: transform 150ms var(--ease-out),
opacity 150ms var(--ease-out);
transform: scale(1);
opacity: 1;
}
The --transform-origin variable, standard in Base UI implementations, dynamically resolves to the trigger's position. Modals remain the exception—they stay centered, so transform-origin: center is appropriate per STANDARDS.md#L55.
Pure-CSS Entrances with @starting-style
When JavaScript state management isn't required, the CSS @starting-style rule provides a clean, compositor-thread solution. The browser applies starting values instantly, then transitions to the final state.
.toast {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms var(--ease-out),
transform 300ms var(--ease-out);
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
This approach adheres to the repository's performance principle: keep animations on GPU-only properties (transform and opacity). STANDARDS.md#L110 explicitly recommends this pattern for optimal rendering performance.
Complete Reference Examples
/* 1️⃣ Standard entrance: scale from 0.95 + fade */
.fade-scale-enter {
transform: scale(0.95);
opacity: 0;
}
.fade-scale-enter[data-mounted] {
transition: transform 200ms var(--ease-out),
opacity 200ms var(--ease-out);
transform: scale(1);
opacity: 1;
}
/* 2️⃣ Trigger-anchored popover */
.popover {
transform-origin: var(--transform-origin);
transform: scale(0.95);
opacity: 0;
}
.popover[data-mounted] {
transition: transform 150ms var(--ease-out),
opacity 150ms var(--ease-out);
transform: scale(1);
opacity: 1;
}
/* 3️⃣ No-JS toast with @starting-style */
.toast {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms var(--ease-out),
transform 300ms var(--ease-out);
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
Source Files and Their Roles
| File | Purpose |
|---|---|
[skills/review-animations/STANDARDS.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) |
Defines the "Never scale(0)" rule and provides the scale-and-opacity pattern |
[skills/emil-design-eng/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) |
Shows concrete before/after examples and explains popover physicality |
[skills/improve-animations/AUDIT.md](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md) |
Lists "Never scale(0)" as high-severity with target values |
[skills/find-animation-opportunities/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/find-animation-opportunities/SKILL.md) |
Describes how to identify and replace scale(0) entrances |
Summary
-
scale(0)violates physical expectations by making elements appear from nothing, creating unnatural, jarring entrances. -
Use
scale(0.95)toscale(0.97)withopacity: 0as the starting state—this preserves visual recognition while maintaining the entrance effect. -
Anchor popovers to their trigger via
transform-origin: var(--transform-origin), keeping spatial continuity intact. -
Prefer
@starting-stylefor pure-CSS implementations to stay on the compositor thread and avoid JavaScript overhead. -
Reference the repository standards in
STANDARDS.md,AUDIT.md, andSKILL.mdfiles for consistent, high-quality animation implementation.
Frequently Asked Questions
What exact scale value should I use instead of scale(0)?
Use between 0.90 and 0.97 depending on the element size and animation speed. Smaller elements (tooltips) benefit from the higher end (0.95–0.97), while larger modals can start slightly smaller (0.90–0.93). The key is maintaining enough visual presence that users recognize the shape before it fully scales in.
Why does scale(0) cause performance problems?
When an element has zero size, the browser must perform full layout recalculation and paint as it scales up, rather than promoting the animation to the compositor thread immediately. The near-zero scale pattern keeps the element's render layer active, allowing the browser to handle the transition via GPU-accelerated transforms alone.
How do I handle modals differently from popovers?
Modals stay centered: use transform-origin: center and scale from 0.95. Popovers and dropdowns anchor to their trigger: use transform-origin: var(--transform-origin) (or calculate the trigger position) so the animation originates from the correct point. This distinction appears in [STANDARDS.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) as a physicality requirement for spatial continuity.
When can I use @starting-style instead of data attributes?
Use @starting-style when the entrance doesn't require JavaScript-driven state—static toasts, CSS-only dropdowns, or page-load animations. Stick with [data-mounted] or class toggles when you need explicit mount/unmount control, exit animations, or React/Vue component lifecycle integration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →