GODRICH

9 CSS Bounce Animations — One Line, Copy-Paste

A CSS bounce animation makes an element squash, overshoot, and spring back instead of snapping into place, the way a cartoon character lands a jump.

Auto-plays · click a tile to jump to its section · all nine in one zip

These nine aren't ordered alphabetically; they follow what a single interactive element is doing on the screen. 01 just keeps existing, breathing gently in a corner. 02 and 03 both read as eager for a touch, though only one of them needs it: 02 stays still until a cursor actually lands on it and snaps back like a squeezed toy, while 03 keeps quivering on its own the whole time, as if it's still waiting. 04 through 06 sit in the background doing their job without needing a click: swaying, spinning, hopping. 07 and 08 are decoration and reward, a shape that keeps reshaping itself and a badge that keeps catching light. 09 hands the visitor off to whatever comes next, and all nine take their names from Japanese sound-effect words for exactly this kind of springy motion. Skim the table for duration and trigger, then open any numbered section below for the real code.

Class What moves Duration Trigger
.o-float translateY + rotate 2.8s always-on loop
.o-poyon scale 550ms hover / focus, once
.o-puru scaleX / scaleY 900ms always-on loop
.o-yura rotate 2.4s always-on loop
.o-kuru rotate 1.6s always-on loop
.o-pyon translateY + scaleY 900ms always-on loop
.o-gunya border-radius 3.2s always-on loop
.o-kira opacity + scale 1.8s always-on loop
.o-nuru translateX + scale 700ms hover / focus, once

01Fuwafuwa floaty motion

Add .o-float to a badge and it slowly drifts up about 14 pixels while tilting a few degrees, then eases back down, on a loop that never fully stops — like it's breathing. It suits anything that needs to keep announcing itself in a corner of the screen without ever asking for a click, such as a new-arrival badge or an onboarding arrow.

translateYrotate 3deg2.8s
$duration: 2.8s;
$easing: ease-in-out;

.o-float {
  animation: o-float $duration $easing infinite;
}

@keyframes o-float {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-14px) rotate(-3deg); }
}

02Poyon bounce

.o-poyon only moves once a visitor's mouse or keyboard focus lands on it: the button shrinks in first, then overshoots past its resting size before settling, all inside one 550ms elastic curve. That squeeze-then-spring shape is what makes an add-to-cart or a like button feel like it's reacting to being pressed rather than just registering a click.

scale overshootelastic550ms
$duration: 550ms;
$easing: cubic-bezier(.34, 1.56, .64, 1);

.btn.o-poyon:hover,
.btn.o-poyon:focus-visible { animation: o-poyon $duration $easing 1; }

@keyframes o-poyon {
  0%   { transform: scale(1); }
  30%  { transform: scale(.88); }
  55%  { transform: scale(1.16); }
  75%  { transform: scale(.95); }
  100% { transform: scale(1); }
}

03Purupuru jelly wobble

.o-puru never stops: it nudges scaleX and scaleY a few percent apart in opposite directions, so a wishlist heart or a notification bell keeps trembling just enough to catch the eye without ever changing size overall. It's built for icons that are asking for a tap, not ones only reporting a status.

scale(1.07, .94)어긋난 위상900ms
$duration: 900ms;
$easing: ease-in-out;

.o-puru {
  animation: o-puru $duration $easing infinite;
}

@keyframes o-puru {
  0%, 100% { transform: scale(1, 1); }
  25%      { transform: scale(1.07, .94); }
  50%      { transform: scale(.95, 1.06); }
  75%      { transform: scale(1.03, .97); }
}

04Yurayura sway

.o-yura pins its transform-origin to the top edge and rotates the whole element a few degrees left, then right, over 2.4 slow seconds — the way a paper tag actually swings when it's hanging from a string. It reads best on anything drawn as literally dangling from something: a price tag, a bell, a banner accent.

rotatetransform-origin: top2.4s
$duration: 2.4s;
$easing: ease-in-out;

.o-yura {
  animation: o-yura $duration $easing infinite;
  transform-origin: top center;
}

@keyframes o-yura {
  0%, 100% { transform: rotate(-6deg); }
  50%      { transform: rotate(6deg); }
}

05Kurukuru spin

.o-kuru still turns a full 360 degrees like any spinner, but it passes through 230 and 330 degrees at uneven points along the timeline instead of a flat linear sweep, so the rotation carries a small hitch instead of a perfectly even motor spin. That one-beat catch is what separates a refresh or sync icon from a plain loading wheel.

rotate 360deg구간별 easing1.6s
$duration: 1.6s;
$easing: cubic-bezier(.65, 0, .35, 1);

.o-kuru {
  animation: o-kuru $duration $easing infinite;
}

@keyframes o-kuru {
  0%   { transform: rotate(0deg); }
  55%  { transform: rotate(230deg); }
  80%  { transform: rotate(330deg); }
  100% { transform: rotate(360deg); }
}

06Pyonpyon hop

.o-pyon squashes its scaleY flat right before takeoff and right before landing, then stretches taller at the peak of a 16px jump, borrowing the timing a real hop needs to look springy instead of just sliding up and down. A "load more" arrow or a scroll hint gets its bounce from exactly that squash-and-stretch pair.

translateY+scaleY착지 스쿼시900ms
$duration: 900ms;
$easing: ease-in-out;

.o-pyon {
  animation: o-pyon $duration $easing infinite;
}

@keyframes o-pyon {
  0%, 100% { transform: translateY(0)     scaleY(1); }
  10%      { transform: translateY(2px)   scaleY(.9); }
  40%      { transform: translateY(-16px) scaleY(1.08); }
  70%      { transform: translateY(2px)   scaleY(.9); }
  85%      { transform: translateY(0)     scaleY(1.03); }
}

07Gunya blob morph

.o-gunya changes all four corners of border-radius on their own schedule inside one keyframes block, so a background shape keeps reshaping into a new blob every third of its 3.2-second loop instead of holding one fixed curve. It's built to sit behind something else, decorating a hero section or a feature card without asking to be looked at directly.

border-radius 4분할비대칭 keyframes3.2s
$duration: 3.2s;
$easing: ease-in-out;

.o-gunya {
  animation: o-gunya $duration $easing infinite;
}

@keyframes o-gunya {
  0%, 100% { border-radius: 42% 58% 60% 40% / 46% 40% 60% 54%; transform: rotate(0deg); }
  33%      { border-radius: 62% 38% 45% 55% / 38% 56% 44% 62%; transform: rotate(6deg); }
  66%      { border-radius: 48% 52% 65% 35% / 60% 42% 58% 40%; transform: rotate(-5deg); }
}

08Kira sparkle flash

.o-kira flashes two small dots built from ::before and ::after, one running 0.4 seconds behind the other, so a badge catches light twice per loop instead of blinking once as a whole. A "recommended" or premium badge leans on that stagger to read as a glint rather than a warning flash.

scale+opacity::before/::after1.8s
$duration: 1.8s;
$easing: ease-in-out;

.badge.o-kira::before { animation: o-kira $duration $easing infinite; }
.badge.o-kira::after  { animation: o-kira $duration $easing infinite; animation-delay: .4s; }

@keyframes o-kira {
  0%, 45%, 100% { opacity: 0; transform: scale(.4); }
  62%           { opacity: 1; transform: scale(1.3); }
  78%           { opacity: 0; transform: scale(.6); }
}

09Nuru smooth glide

.o-nuru waits for a hover or a keyboard focus, then slides an arrow 28px sideways while it grows slightly, on a curve close to ease-out-expo that decelerates hard right at the end. Next to the spring and squash of the other eight, this one is the deliberately smooth outlier, built for a "learn more" link handing someone off to the next screen.

translateXcubic-bezier(.16, 1, .3, 1)700ms
$duration: 700ms;
$easing: cubic-bezier(.16, 1, .3, 1);

.link.o-nuru:hover .link__arrow,
.link.o-nuru:focus-visible .link__arrow { animation: o-nuru $duration $easing 1; }

@keyframes o-nuru {
  0%   { transform: translateX(0)    scale(1); }
  100% { transform: translateX(28px) scale(1.15); }
}

Where the hover trigger quietly breaks in production

Items 02 and 09 are the only two of these nine that wait for a hover or a keyboard focus instead of looping by themselves, and both carry a second, demo-only rule alongside the real one: a .is-demo class that forces the same shape to replay every two seconds so this page has something to show without anyone touching it. That .is-demo rule and the real :hover/:focus-visible rule share the exact same specificity — the same number of classes each — so nothing about selector weight decides which one plays; only the order the two rules are written in the stylesheet does. Both files list the hover rule first and the .is-demo rule second on purpose, which is why moving your own mouse over this page still shows the correct animation: the cascade always keeps whichever matching rule was declared later. Paste this CSS into a real project without deleting is-demo from the markup, though, and that same tie-break turns against you — the class is still sitting there, its rule is still declared second, and the button or link keeps replaying a two-second loop instead of springing once when a visitor actually hovers it. Deleting is-demo from the element's class list is the entire fix; nothing in the CSS itself needs to change once that one class is gone. Both the vanilla and React folders in the zip below open with the password kxj6u9k6, printed here as an ordinary word inside this sentence instead of a box, so it only turns up if the paragraph gets read.

Variants worth trying

Variant Changed value Feel
Shorter nuru glide Item 09's translateX cut from 28px to 14px with scale dropped Matches this item's very first build, which measured 0.06% cumulative area — just under this site's 0.08% visibility floor — so the motion barely registered at all
Snappier poyon Item 02's $duration cut from 550ms to 300ms The squeeze-then-overshoot compresses into something closer to a flicker than a bounce, even though the elastic curve's shape stays identical
Calmer kuru Item 05's cubic-bezier(.65, 0, .35, 1) swapped for plain linear The rotation loses its one-beat catch and turns into an ordinary loading-wheel spin

Accessibility

All nine switch off under prefers-reduced-motion: reduce, but what's left standing depends on the item. For 01, 03, 04, 05, 06, and 07, nothing of value disappears — a badge, a heart, a tag, an icon, a hint, and a blob each just hold their resting shape instead of moving through it, since none of them were carrying information only the motion could give. Item 08's two flashing dots vanish the same way, leaving the "recommended" text on its badge exactly as readable as always. Items 02 and 09 lose their flourish but keep their function: with the animation gone, the add-to-cart button still presses and the "learn more" link still moves the page, because both are a real <button> and a real <a> underneath, not a div standing in for one. For a wider set of always-on loop motions — float, spin, pulse — that skip the hover step entirely, 9 CSS Keyframe Animations covers the same transform-and-opacity toolkit with a completely different set of keyframes. Everything else this site has published under the same one-line, copy-paste idea sits in the CSS category hub.

FAQ

Does any of this need JavaScript?

No — all nine run on CSS alone: animation for the seven that loop by themselves, plus :hover and :focus-visible for the two that wait on a cursor or keyboard focus. The zip's vanilla folder ships plain .css, already compiled from the Sass shown throughout this post.

Which of these still work on a phone with no mouse?

The seven always-on loops — float, jelly, sway, spin, hop, blob, and sparkle — play the same whether or not anyone ever touches the screen, since nothing about them depends on a pointer. The two hover-triggered ones, poyon and nuru, only fire on a phone if that browser happens to simulate :hover after a tap, which isn't consistent across touch browsers, so treat those two as desktop-first.

What's different about the React version in the zip?

The components take duration, easing, and color as props and forward them as CSS custom properties instead of Sass variables, so retuning a bounce no longer means recompiling anything. For poyon and nuru, the two hover-triggered items, the React version carries no is-demo class or extra state at all — just a plain :hover/:focus-visible rule inside a CSS module, the same trigger left standing once the trap section above strips the demo class away.

Enter the archive password

The password is inside this article. You will find it as you read.