16 CSS Bounce Animations02 / 16
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
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><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);
}
}
}.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');
});
})();(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');
});
})();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');
});
})();
```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.1is subtle,1.25is dramatic,1.4reads 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)ortranslateX(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 intailwind.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/heightto scale the card — it triggers layout+paint on every frame. Always usetransform: 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
bothkeyword inanimation: bn-02-bounce-in .75s cubic-bezier(.34,1.56,.64,1) bothcombinesforwards+backwards— it applies the 100% state after animation ends AND the 0% state before it starts. Withoutboth, the card would flash visible for one frame before the animation begins.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.