Home / Snippets / Animation /

Bounce in

Entrance animation that scales elements from zero with an elastic overshoot for a playful, spring-like reveal — no JavaScript.

🎯
Focus
Delight
🚀
Speed
Accessible
Widely Supported
animationno-js

Quick implementation

.bounce-in {
  animation: bounce-in 0.6s ease both;
}

@keyframes bounce-in {
  0%   { transform: scale(0);    opacity: 0; }
  60%  { transform: scale(1.15); opacity: 1; }
  80%  { transform: scale(0.95);             }
  100% { transform: scale(1);    opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .bounce-in {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building entrance animations for a UI.

Goal: A bounce-in entrance animation that scales elements from zero to their
natural size with an elastic overshoot — giving a spring-like, playful feel
with no JavaScript.

Technical constraints:
- Use a multi-step @keyframes:
    0%   { transform: scale(0);    opacity: 0; }
    60%  { transform: scale(1.15); opacity: 1; }
    80%  { transform: scale(0.95);             }
    100% { transform: scale(1);    opacity: 1; }
- Apply animation: bounce-in 0.6s ease both; — the "both" fill mode keeps
  the element invisible and scaled to zero before the animation starts and
  holds the final state after it ends.
- Use oklch() for any colors — no hex or rgba values.
- Use CSS custom properties (var(--card), var(--text), etc.) for theming.
- Include @media (prefers-reduced-motion: reduce) that sets animation: none,
  opacity: 1, and transform: none on the animated element.

Framework variant (pick one):
A) Vanilla CSS class .bounce-in applicable to any element, with
   :nth-child() selectors for staggered animation-delay values.
B) React component — accept children, delay (number in seconds), and
   duration props; apply the class and inline animation-delay via style.

Edge cases to handle:
- Elements that start at scale(0) are invisible but still occupy space in
  the layout; if they flash or cause layout shift before animating, ensure
  animation-fill-mode is set to "both" and not "forwards" alone.
- Overshoot values above ~1.2 can feel jarring; keep scale at the 60%
  keyframe between 1.1 and 1.2 for a natural spring feel.
- Staggered lists should use animation-delay increments (e.g. 0.1s per item)
  rather than re-running the animation via JavaScript class toggling.
- The transform-origin defaults to center — if the element should bounce in
  from a corner or edge, set transform-origin explicitly.

Return CSS only (or a React component if variant B is chosen).

Why bounce feels natural

The bounce-in pattern draws directly from the physics of elastic materials. When a real spring-loaded object snaps into place, it doesn't stop dead at the target — it overshoots, compresses back slightly, then settles. The keyframe sequence mimics this: the element grows past its intended size at 60%, pulls back a little at 80%, then lands at exactly scale(1). That two-step overshoot-and-recover is what makes the animation feel alive rather than mechanical.

Standard CSS easing functions like ease-in-out approximate this for position, but for scale-based entrance animations the multi-step @keyframes approach gives you direct control over each inflection point. You decide exactly how far the element overshoots and how quickly it recovers — without needing cubic-bezier() values or a JavaScript spring library.

The multi-step keyframe logic

The four keyframe stops each serve a distinct purpose. At 0%, scale(0) and opacity: 0 make the element completely invisible — it takes up no visual space. By 60% the element has already become fully opaque and overshot to scale(1.15), 15% larger than its resting size. This overshoot happens in the first 60% of the duration, so most of the motion is front-loaded and fast.

At 80%, the scale pulls back to scale(0.95) — a slight undershoot that creates the elastic snap. The final 100% keyframe lands cleanly at scale(1). The ease timing function applies within each segment, so the overall curve is smooth rather than linear.

The both fill mode on animation is load-bearing: it applies the 0% values before the animation fires (keeping the element invisible until it's time to bounce in) and locks the 100% values in place after the animation finishes (so the element doesn't disappear when the keyframe ends).

Performance & accessibility

Like all animations that rely exclusively on transform and opacity, bounce-in runs entirely on the compositor thread. The browser hands it off to the GPU and animates it independently of the main thread. No layout recalculations, no repaints — the animation stays smooth even when JavaScript is busy or the page is content-heavy.

Staggering via animation-delay on :nth-child() selectors is the right approach for lists of bouncing elements. Incrementing by about 0.1–0.15 s per item gives a pleasing cascade without making users wait too long for the last item. Avoid toggling classes from JavaScript just to replay the animation; instead, briefly set animation: none, force a reflow with offsetHeight, then restore the class — as the replay button on this page demonstrates.

The prefers-reduced-motion: reduce block is non-negotiable. The rapid scale change of a bounce can be disorienting for users with vestibular disorders. Setting animation: none, opacity: 1, and transform: none inside the query ensures those users see every element in its final visible state instantly — no motion, no flash of invisible content caused by the both fill mode.