---
name: elastic-hover
description: Use when you want "Friendly, lively, tactile" - Uses spring-like scaling or stretching when elements are hovered.
---

# Elastic Hover

> **Category:** Motion / Interaction  -  **Personality:** Friendly, lively, tactile
>
> **Best use:** Buttons, cards, playful interfaces
>
> **Ratings:** Professional 3/5 - Casual 5/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

Elastic Hover makes an element feel like a physical, springy object: on hover it scales up and **overshoots** its target size before settling, and the best ones add a little **squash-&-stretch** (it briefly gets wide-and-short, then tall-and-thin, then settles) so the motion reads as elastic rather than linear. The whole thing is just `transform` plus a bouncy easing curve — no library. It signals "this is tappable and fun", which is why playful buttons, reaction icons and cards lean on it.

There are two halves, and you can use either or both:
1. **Overshoot spring** — a normal `transition` with an *over-1* cubic-bezier. Clean enter/leave, great for buttons and cards.
2. **Squash-&-stretch jelly** — a short `@keyframes` run on `:hover` that distorts `scaleX`/`scaleY` independently. More cartoonish; great for icons.

## Dependencies / CDN

None - pure CSS for the effect (the demo's JS is an optional click flourish). Display/body fonts are optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
```

## HTML

Any element works — a button, an icon tile, a card:

```html
<button class="eh-btn eh-btn-solid">Start bouncing</button>

<button class="eh-icon" aria-label="React with fire">&#128293;</button>

<article class="eh-card" tabindex="0">
  <div class="eh-card-emoji">&#127919;</div>
  <h3>Today&rsquo;s quest</h3>
</article>
```

## CSS

```css
:root{ --eh-spring: cubic-bezier(.34,1.56,.64,1); } /* the >1 overshoot is the whole trick */

/* 1) OVERSHOOT SPRING — buttons & cards. The bouncy curve makes scale spring past then settle. */
.eh-btn{
  border:0; border-radius:999px; padding:11px 20px; cursor:pointer;
  will-change:transform;
  transition:transform .5s var(--eh-spring), box-shadow .3s ease;
}
.eh-btn:hover{ transform:scale(1.07); }      /* springs up with a little overshoot */
.eh-btn:active{ transform:scale(.92,.96); }  /* press = squash → tactile "click" */

.eh-card{
  border-radius:22px; cursor:pointer; outline:0;
  will-change:transform;
  transition:transform .55s var(--eh-spring), box-shadow .4s ease;
}
.eh-card:hover,
.eh-card:focus-visible{ transform:translateY(-10px) scale(1.03) rotate(-.7deg); }

/* 2) SQUASH-&-STRETCH JELLY — icons. A keyframe distorts scaleX/scaleY, ending at the hover size. */
.eh-icon{
  width:52px; height:52px; border:0; border-radius:17px; cursor:pointer;
  display:inline-grid; place-items:center;
  will-change:transform;
  transition:transform .5s var(--eh-spring);   /* drives the springy return on hover-out */
}
.eh-icon:hover{ animation:eh-jelly .62s both; } /* plays once, then "both" holds the end frame */

@keyframes eh-jelly{
  0%  { transform:scale(1)        }
  28% { transform:scale(1.26,.78) }  /* squash: wide + short */
  44% { transform:scale(.82,1.18) }  /* stretch: narrow + tall */
  60% { transform:scale(1.12,.92) }
  74% { transform:scale(.96,1.05) }
  86% { transform:scale(1.04,.99) }
  100%{ transform:scale(1.08)     }  /* settle at the hovered size (matches the leave transition) */
}

/* 3) GENTLE under reduced-motion: no overshoot, no jelly, small soft scale */
@media (prefers-reduced-motion: reduce){
  .eh-btn, .eh-icon, .eh-card{ transition:transform .2s ease !important; animation:none !important; }
  .eh-btn:hover { transform:scale(1.03) !important; }
  .eh-icon:hover{ transform:scale(1.04) !important; }
  .eh-card:hover{ transform:translateY(-5px) !important; }
}
```

## JavaScript

Not required — hover/press is pure CSS. The demo adds two small flourishes: re-trigger the bounce on **click** (the `:hover` animation has already finished mid-hover), and keep a "like" count.

```js
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

// Restart the CSS :hover animation by nulling it, forcing a reflow, then handing it back.
function rebounce(el){
  if(reduce) return;
  el.style.animation = 'none';
  void el.offsetWidth;        // reflow — without this the browser coalesces the change away
  el.style.animation = '';
}

document.querySelectorAll('.eh-icon').forEach(function(el){
  el.addEventListener('click', function(){
    rebounce(el);
    if(el.hasAttribute('data-like')){
      var liked = el.classList.toggle('is-liked');
      el.setAttribute('aria-pressed', liked ? 'true' : 'false');
      var c = el.querySelector('.eh-like-count');
      if(c) c.textContent = (parseInt(c.textContent,10)||0) + (liked ? 1 : -1);
    }
  });
});
```

## How it works

- **The overshoot lives in the easing curve.** `cubic-bezier(.34,1.56,.64,1)` has a second control-point Y of `1.56` (> 1), so the animated value travels *past* its target and eases back — that "past then back" is the spring. Swap in a plain `ease` and the bounce vanishes.
- **Squash-&-stretch needs independent axes.** `transform:scale(1.26,.78)` widens while flattening (conserving apparent "volume"), the next frame does the opposite. Animating `scaleX`/`scaleY` out of phase is what sells "elastic" over "just bigger".
- **Animation-then-transition hand-off.** The jelly keyframe ends at `scale(1.08)` — the same size the element rests at while hovered. So when the pointer leaves, the `@keyframes` rule disappears and the base `transition` springs `1.08 → 1`. Matching those two values keeps the leave smooth with no jump.
- **`transform` only.** Scaling is GPU-composited and never triggers layout/paint, so it stays at 60fps even on many elements.

## Customization

- **Bounciness:** raise the curve's third number for more overshoot — `cubic-bezier(.34,1.8,.64,1)` is springier, `(.2,1.2,.3,1)` is subtle. Or use the named `linear()` springs if you target modern browsers only.
- **Pro vs playful:** buttons/cards → overshoot scale only (tasteful). Icons/mascots → add the jelly keyframe (cartoonish).
- **Press depth:** tune `:active{transform:scale(.92,.96)}` — more squash = chunkier "give".
- **Lift:** combine `translateY(-10px)` with `scale()` and a deeper `box-shadow` so cards feel like they pop off the page; a tiny `rotate(-.7deg)` adds personality.
- **Duration:** keep it short (.45–.6s). Longer reads as laggy, not springy.

## Accessibility & performance

- **Reduced motion:** gate it behind `@media (prefers-reduced-motion: reduce)` — drop the overshoot and the jelly, keep a small `.2s ease` scale (gentle, no bounce). The demo also checks `matchMedia` in JS before re-triggering.
- **Animate only `transform`** (and `box-shadow`/`opacity`). Never animate `width`/`height`/`top` — those thrash layout. Add `will-change:transform` to elements that bounce often.
- **Keyboard parity:** mirror `:hover` with `:focus-visible` so keyboard users get the same feedback (the cards do this), and keep a visible focus ring.
- **Don't scale text-heavy blocks much** — a wobbling paragraph hurts legibility; reserve the strong jelly for icons/controls.

## Gotchas

- **A `:hover` keyframe won't replay on click** (it's already finished while you hover). To re-fire it you must null the `animation`, force a reflow (`void el.offsetWidth`), then restore it — see the JS.
- **Make the jelly's `100%` equal the hover rest size** (`scale(1.08)` here). If `0%`/`100%` are `scale(1)` you get a visible snap to/from the hovered transition value on leave.
- **Touch devices have no real hover** — `:hover` may stick or never fire. Provide a tap/`:active` state (or a JS click bounce) so phones still feel responsive.
- **Overshoot can clip.** A scaled element growing past a parent with `overflow:hidden` gets cut off — give it room or scale down slightly.
- **`transform` creates a stacking context**, so a bouncing card can jump above neighbours mid-animation; set `position:relative` + a `z-index` if ordering matters.
