16 CSS Bounce Animations02 / 16

CSS + JSMIT licensed

Bounce-In Entrance

The canonical bounceIn entrance every animate.css user has copy-pasted at some point, rewritten as vanilla CSS with a modern cubic-bezier + 5-stop keyframe. A single centered card scales in from 0 with elastic overshoot then settles. Perfect drop-in replacement for the animate__bounceIn class most tutorials still teach — no 20KB library import needed. Click the trigger to replay the entrance.

Published

Live Demo
Try it

The code

<div class="bn-02">
  <button class="bn-02__trigger" id="bn-02-open" type="button">Replay bounce-in</button>
  <div class="bn-02__backdrop" id="bn-02-stage">
    <div class="bn-02__card">
      <span class="bn-02__badge" aria-hidden="true">✨</span>
      <h2 class="bn-02__title">bounceIn</h2>
      <p class="bn-02__sub">Vanilla CSS replica of the animate.css <code>bounceIn</code> class — 5-stop keyframe, 750ms, cubic-bezier(.34,1.56,.64,1).</p>
    </div>
  </div>
</div>
.bn-02 {
  --page-bg: #f8fafc;
  --card: #ffffff;
  --ink: #0f172a;
  --sub: #64748b;
  --brand: #635bff;
  --brand-dark: #4f46e5;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  padding: 32px;
  background: radial-gradient(60% 60% at 20% 20%, rgba(99,91,255,.08) 0%, transparent 55%), var(--page-bg);
  color: var(--ink);
}

.bn-02 *,
.bn-02 *::before,
.bn-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-02 ::selection {
  background: var(--brand);
  color: #fff;
}

.bn-02__trigger {
  position: absolute;
  top: 32px;
  left: 50%;
  transform: translateX(-50%);
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  background: var(--brand);
  color: #fff;
  font-family: inherit;
  font-size: .85rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 18px -6px rgba(99,91,255,.4);
  transition: background .18s ease;
  z-index: 2;
}

.bn-02__trigger:hover {
  background: var(--brand-dark);
}

.bn-02__backdrop {
  display: flex;
  align-items: center;
  justify-content: center;
  width: min(520px,100%);
  padding: 60px 40px;
  border-radius: 18px;
  background: rgba(99,91,255,.06);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.bn-02__card {
  padding: 36px 32px;
  border-radius: 14px;
  background: var(--card);
  box-shadow: 0 20px 40px -12px rgba(15,23,42,.16);
  text-align: center;
  max-width: 340px;
  opacity: 0;
  transform: scale(.3);
  animation: bn-02-bounce-in .75s cubic-bezier(.34,1.56,.64,1) both;
}

.bn-02__backdrop.is-replay .bn-02__card {
  animation: none;
  animation: bn-02-bounce-in .75s cubic-bezier(.34,1.56,.64,1) both;
}

@keyframes bn-02-bounce-in {
  0% {
    opacity: 0;
    transform: scale(.3);
  }

  20% {
    transform: scale(1.1);
  }

  40% {
    transform: scale(.9);
  }

  60% {
    opacity: 1;
    transform: scale(1.03);
  }

  80% {
    transform: scale(.97);
  }

  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.bn-02__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: linear-gradient(135deg,#a78bfa,#635bff);
  color: #fff;
  font-size: 22px;
  margin-bottom: 16px;
}

.bn-02__title {
  font-family: 'JetBrains Mono',ui-monospace,monospace;
  font-size: 1.6rem;
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.1;
  margin-bottom: 8px;
}

.bn-02__sub {
  font-size: .85rem;
  color: var(--sub);
  line-height: 1.5;
}

.bn-02__sub code {
  font-family: 'JetBrains Mono',ui-monospace,monospace;
  font-size: .78rem;
  padding: 1px 5px;
  border-radius: 4px;
  background: #f1f5f9;
  color: var(--brand);
}

@media (prefers-reduced-motion: reduce) {
  .bn-02__card,
    .bn-02__backdrop.is-replay .bn-02__card {
    animation: bn-02-fade .3s ease-out both!important;
  }

  @keyframes bn-02-fade {
    from {
      opacity: 0;
    }

    to {
      opacity: 1;
      transform: scale(1);
    }
  }
}
(function(){
  var root = document.querySelector('.bn-02');
  if (!root) return;
  var btn = root.querySelector('#bn-02-open');
  var stage = root.querySelector('#bn-02-stage');
  if (!btn || !stage) return;
  btn.addEventListener('click', function(){
    stage.classList.remove('is-replay');
    void stage.offsetWidth;
    stage.classList.add('is-replay');
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Bounce Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Bounce-In Entrance
Source: https://codefronts.com/motion/css-bounce-animation/bounce-in-entrance/

The canonical bounceIn entrance every animate.css user has copy-pasted at some point, rewritten as vanilla CSS with a modern cubic-bezier + 5-stop keyframe. A single centered card scales in from 0 with elastic overshoot then settles. Perfect drop-in replacement for the animate__bounceIn class most tutorials still teach — no 20KB library import needed. Click the trigger to replay the entrance.
## HTML
```html
<div class="bn-02">
  <button class="bn-02__trigger" id="bn-02-open" type="button">Replay bounce-in</button>
  <div class="bn-02__backdrop" id="bn-02-stage">
    <div class="bn-02__card">
      <span class="bn-02__badge" aria-hidden="true">✨</span>
      <h2 class="bn-02__title">bounceIn</h2>
      <p class="bn-02__sub">Vanilla CSS replica of the animate.css <code>bounceIn</code> class — 5-stop keyframe, 750ms, cubic-bezier(.34,1.56,.64,1).</p>
    </div>
  </div>
</div>
```
## CSS
```css
.bn-02 {
  --page-bg: #f8fafc;
  --card: #ffffff;
  --ink: #0f172a;
  --sub: #64748b;
  --brand: #635bff;
  --brand-dark: #4f46e5;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  padding: 32px;
  background: radial-gradient(60% 60% at 20% 20%, rgba(99,91,255,.08) 0%, transparent 55%), var(--page-bg);
  color: var(--ink);
}

.bn-02 *,
.bn-02 *::before,
.bn-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-02 ::selection {
  background: var(--brand);
  color: #fff;
}

.bn-02__trigger {
  position: absolute;
  top: 32px;
  left: 50%;
  transform: translateX(-50%);
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  background: var(--brand);
  color: #fff;
  font-family: inherit;
  font-size: .85rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 18px -6px rgba(99,91,255,.4);
  transition: background .18s ease;
  z-index: 2;
}

.bn-02__trigger:hover {
  background: var(--brand-dark);
}

.bn-02__backdrop {
  display: flex;
  align-items: center;
  justify-content: center;
  width: min(520px,100%);
  padding: 60px 40px;
  border-radius: 18px;
  background: rgba(99,91,255,.06);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.bn-02__card {
  padding: 36px 32px;
  border-radius: 14px;
  background: var(--card);
  box-shadow: 0 20px 40px -12px rgba(15,23,42,.16);
  text-align: center;
  max-width: 340px;
  opacity: 0;
  transform: scale(.3);
  animation: bn-02-bounce-in .75s cubic-bezier(.34,1.56,.64,1) both;
}

.bn-02__backdrop.is-replay .bn-02__card {
  animation: none;
  animation: bn-02-bounce-in .75s cubic-bezier(.34,1.56,.64,1) both;
}

@keyframes bn-02-bounce-in {
  0% {
    opacity: 0;
    transform: scale(.3);
  }

  20% {
    transform: scale(1.1);
  }

  40% {
    transform: scale(.9);
  }

  60% {
    opacity: 1;
    transform: scale(1.03);
  }

  80% {
    transform: scale(.97);
  }

  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.bn-02__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: linear-gradient(135deg,#a78bfa,#635bff);
  color: #fff;
  font-size: 22px;
  margin-bottom: 16px;
}

.bn-02__title {
  font-family: 'JetBrains Mono',ui-monospace,monospace;
  font-size: 1.6rem;
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.1;
  margin-bottom: 8px;
}

.bn-02__sub {
  font-size: .85rem;
  color: var(--sub);
  line-height: 1.5;
}

.bn-02__sub code {
  font-family: 'JetBrains Mono',ui-monospace,monospace;
  font-size: .78rem;
  padding: 1px 5px;
  border-radius: 4px;
  background: #f1f5f9;
  color: var(--brand);
}

@media (prefers-reduced-motion: reduce) {
  .bn-02__card,
    .bn-02__backdrop.is-replay .bn-02__card {
    animation: bn-02-fade .3s ease-out both!important;
  }

  @keyframes bn-02-fade {
    from {
      opacity: 0;
    }

    to {
      opacity: 1;
      transform: scale(1);
    }
  }
}
```

## JavaScript
```js
(function(){
  var root = document.querySelector('.bn-02');
  if (!root) return;
  var btn = root.querySelector('#bn-02-open');
  var stage = root.querySelector('#bn-02-stage');
  if (!btn || !stage) return;
  btn.addEventListener('click', function(){
    stage.classList.remove('is-replay');
    void stage.offsetWidth;
    stage.classList.add('is-replay');
  });
})();
```

How this works

The card starts at opacity: 0 and transform: scale(.3) (small + invisible). The @keyframes bn-02-bounce-in uses a 5-stop cascade that faithfully replicates the animate.css bounceIn feel: 0% opacity: 0, scale(.3) → 20% scale(1.1) → 40% scale(.9) → 60% opacity: 1, scale(1.03) → 80% scale(.97) → 100% opacity: 1, scale(1). Two-and-a-half oscillations that damp to the final scale. Duration 750ms with cubic-bezier(.34, 1.56, .64, 1).

Why 5 stops not 3: a 3-stop bounce feels mechanical (rise-peak-settle). A 5-stop bounce with alternating over/undershoot (scale 1.1 → .9 → 1.03 → .97 → 1) reads as physical — like a rubber ball that oscillates around its rest position before settling. This is exactly the pattern animate.css uses, but the animate.css version imports 20KB of CSS. Our recipe is 12 lines of scoped CSS.

The demo shows the effect purely — no elaborate modal layouts, no pricing tables. Just the card with a title labeling what happened, a trigger to replay, and the classic backdrop-blur so users see the card sitting on a scrim. This is the effect in its purest form.

Make it yours

  • Change the entrance intensity by editing the peak scale — 1.1 is subtle, 1.25 is dramatic, 1.4 reads as goofy. Match to your brand's motion vocabulary.
  • For a slide-and-bounce combo (Instagram Story reveal pattern), add translateY(20px → 0) to each keyframe stop alongside the scale.
  • For directional bounce entries (bounceInDown / bounceInUp / bounceInLeft / bounceInRight), combine translateY(-40px) or translateX(40px) at 0% with the scale bounce — the classic animate.css variants become 4-line keyframes.
  • For sober contexts (fintech / medical), swap for a 200ms opacity fade with no scale — see css-modals collection Demo 05 HIPAA for the sober alternative.
  • For Tailwind arbitrary values: animate-[bn02_.75s_cubic-bezier(.34,1.56,.64,1)_both] with the keyframe registered in tailwind.config.js. See FAQ #4.

Gotchas — read before shipping

  • The 5-stop keyframe is deliberate — a 3-stop version feels mechanical. If you flatten to 0% → 50% → 100%, the element enters smoothly but with no physical energy. Users read "boring UI" instead of "delightful arrival." The extra 2 stops cost nothing at runtime.
  • NEVER animate width / height to scale the card — it triggers layout+paint on every frame. Always use transform: scale() which runs on the compositor.
  • The backdrop MUST NOT bounce. If both the backdrop AND card bounce, users get motion sickness — two competing motion signals with different timing curves. Only the primary interactive element bounces; the backdrop is a calm 200ms opacity fade.
  • For WCAG 2.1 dialog compliance when this is used on a real modal, add role='dialog' + aria-modal='true' + aria-labelledby + focus trap + Escape closes + focus return. See css-modals collection for the full pattern.
  • For prefers-reduced-motion, replace the bounce with a 200ms opacity fade + no scale. The element still appears — you preserve the interaction — but decorative motion is disabled.
  • The both keyword in animation: bn-02-bounce-in .75s cubic-bezier(.34,1.56,.64,1) both combines forwards + backwards — it applies the 100% state after animation ends AND the 0% state before it starts. Without both, the card would flash visible for one frame before the animation begins.

Browser support

ChromeSafariFirefoxEdge
76+9+103+76+

Floor set by backdrop-filter as this demo is written. The core technique itself goes back further — Chrome 45+.

transform + opacity + @keyframes. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…