16 CSS Bounce Animations06 / 16

Pure CSSMIT licensed

Button Hover Bounce

A primary CTA button with a hover-triggered bounce micro-interaction — the button lifts and settles with elastic overshoot when the pointer arrives, giving it a physical, clickable feel. Warm orange (#f97316) e-commerce CTA color, satisfying tactile feedback the moment users hover. The interaction reads as "press me" without the intrusive infinite pulse of always-on CTAs.

Published

Live Demo
Try it

The code

<div class="bn-06">
  <p class="bn-06__label">Hover the button</p>
  <button class="bn-06__cta" type="button">
    <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <circle cx="9" cy="21" r="1"/>
      <circle cx="20" cy="21" r="1"/>
      <path d="M1 1h4l2.7 13.4a2 2 0 002 1.6h9.7a2 2 0 002-1.6L23 6H6"/>
    </svg>
    <span>Add to cart · $168</span>
  </button>
  <p class="bn-06__hint">The button lifts + squashes on hover, then settles.</p>
</div>
.bn-06 {
  --page-bg: #fef7ed;
  --ink: #3a2a1a;
  --sub: #8b7355;
  --brand: #f97316;
  --brand-dark: #ea580c;
  --brand-deep: #c2410c;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  background: radial-gradient(70% 60% at 20% 20%, rgba(249,115,22,.08) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

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

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

.bn-06__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-06__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px 34px;
  border: none;
  border-radius: 11px;
  background: var(--brand);
  color: #fff;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 12px 28px -8px rgba(249,115,22,.5);
  transition: background .18s ease,box-shadow .18s ease;
  will-change: transform;
}

.bn-06__cta:hover {
  background: var(--brand-dark);
  box-shadow: 0 16px 36px -8px rgba(249,115,22,.55);
}

.bn-06__cta:active {
  background: var(--brand-deep);
}

.bn-06__cta:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 4px;
}

@media (hover: hover) {
  .bn-06__cta:hover {
    animation: bn-06-hover .38s cubic-bezier(.34,1.56,.64,1) forwards;
  }

  @keyframes bn-06-hover {
    0% {
      transform: translateY(0) scale(1);
    }

    40% {
      transform: translateY(-6px) scaleY(.96) scaleX(1.04);
    }

    100% {
      transform: translateY(0) scale(1);
    }
  }
}

.bn-06__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 320px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .bn-06__cta {
    animation: none!important;
    transform: none!important;
  }

  @media (hover: hover) {
    .bn-06__cta:hover {
      animation: none!important;
    }
  }
}
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: Button Hover Bounce
Source: https://codefronts.com/motion/css-bounce-animation/button-hover-bounce/

A primary CTA button with a hover-triggered bounce micro-interaction — the button lifts and settles with elastic overshoot when the pointer arrives, giving it a physical, clickable feel. Warm orange (#f97316) e-commerce CTA color, satisfying tactile feedback the moment users hover. The interaction reads as "press me" without the intrusive infinite pulse of always-on CTAs.
## HTML
```html
<div class="bn-06">
  <p class="bn-06__label">Hover the button</p>
  <button class="bn-06__cta" type="button">
    <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <circle cx="9" cy="21" r="1"/>
      <circle cx="20" cy="21" r="1"/>
      <path d="M1 1h4l2.7 13.4a2 2 0 002 1.6h9.7a2 2 0 002-1.6L23 6H6"/>
    </svg>
    <span>Add to cart · $168</span>
  </button>
  <p class="bn-06__hint">The button lifts + squashes on hover, then settles.</p>
</div>
```
## CSS
```css
.bn-06 {
  --page-bg: #fef7ed;
  --ink: #3a2a1a;
  --sub: #8b7355;
  --brand: #f97316;
  --brand-dark: #ea580c;
  --brand-deep: #c2410c;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  background: radial-gradient(70% 60% at 20% 20%, rgba(249,115,22,.08) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

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

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

.bn-06__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-06__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px 34px;
  border: none;
  border-radius: 11px;
  background: var(--brand);
  color: #fff;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 12px 28px -8px rgba(249,115,22,.5);
  transition: background .18s ease,box-shadow .18s ease;
  will-change: transform;
}

.bn-06__cta:hover {
  background: var(--brand-dark);
  box-shadow: 0 16px 36px -8px rgba(249,115,22,.55);
}

.bn-06__cta:active {
  background: var(--brand-deep);
}

.bn-06__cta:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 4px;
}

@media (hover: hover) {
  .bn-06__cta:hover {
    animation: bn-06-hover .38s cubic-bezier(.34,1.56,.64,1) forwards;
  }

  @keyframes bn-06-hover {
    0% {
      transform: translateY(0) scale(1);
    }

    40% {
      transform: translateY(-6px) scaleY(.96) scaleX(1.04);
    }

    100% {
      transform: translateY(0) scale(1);
    }
  }
}

.bn-06__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 320px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .bn-06__cta {
    animation: none!important;
    transform: none!important;
  }

  @media (hover: hover) {
    .bn-06__cta:hover {
      animation: none!important;
    }
  }
}
```

How this works

The bounce is hover-triggered, not infinite — it fires only when the user hovers. On :hover, the button animates via a single-shot 3-stop keyframe: 0% translateY(0) → 40% translateY(-6px) scaleY(.96) scaleX(1.04) → 100% translateY(0) scale(1). Duration 380ms with cubic-bezier(.34, 1.56, .64, 1). The mid-stop with scaleY(.96) scaleX(1.04) is a MINI squash-and-stretch — the button briefly flattens horizontally as it lifts, mimicking rubber compression. This is the detail that separates "good" hover bounces from "amazing" ones.

Guard: @media (hover: hover) wraps the hover trigger. Touch devices fire a phantom hover on tap then :active, so touch users would see the bounce fire once before navigation. The guard skips the animation on touch entirely.

The demo centers the button as the hero at ~280px wide, with a single-line label above signaling context. No product card wrapping.

Make it yours

  • Change the bounce intensity by editing translateY(-6px) — 3px for subtle, 10px for playful.
  • Change the squash-stretch ratio via scaleY(.96) scaleX(1.04). More extreme (.9/1.1) reads as bouncy; less (.98/1.02) reads as subtle.
  • For a click-triggered bounce instead of hover (touch-friendly), swap :hover for :active. Bounce fires on click, matches iOS button-press pattern.
  • For a focus-triggered variant (keyboard accessibility), apply the same animation via :focus-visible. Keyboard users get the same feedback when they Tab.
  • For Tailwind: hover:animate-[bn06_.38s_cubic-bezier(.34,1.56,.64,1)_forwards] + keyframes registered. See FAQ #4.

Gotchas — read before shipping

  • Wrap the hover trigger in @media (hover: hover) — touch devices fire a phantom :hover on tap before navigation. The guard skips the animation on touch entirely.
  • Use animation not transition for multi-stop bounce curves. transition linearly interpolates between A and B — you cannot get a bounce peak this way.
  • For accessibility, the hover bounce is DECORATIVE. Screen-reader users don't need it. Keyboard users benefit from a matching :focus-visible variant.
  • Do NOT apply the bounce to the button's TEXT color or background color — those trigger paint on every frame. Only transform is compositor-safe.
  • The squash-and-stretch mid-stop must PRESERVE the button's visual footprint — horizontal expansion (scaleX(1.04)) matched by vertical compression (scaleY(.96)) so mass looks conserved.
  • For touch-device parity, add a :active variant so tap gives visual feedback even without hover.
  • For prefers-reduced-motion, swap the bounce for an instant color/background shift on hover — button still gives feedback via color change.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

transform + @keyframes + @media (hover: hover). Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…