Jelly Icon Dock Hover — Spring Physics Icon Snippet
Jelly Icon Dock Hover · Animations · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="jd-stage">
<p class="jd-hint">Hover each icon — it squashes down then stretches up with real spring physics, like soft rubber</p>
<div class="jd-dock" id="jdDock">
<button class="jd-icon" style="background:#6366f1">🏠</button>
<button class="jd-icon" style="background:#22c55e">💬</button>
<button class="jd-icon" style="background:#f59e0b">🎵</button>
<button class="jd-icon" style="background:#ec4899">📷</button>
<button class="jd-icon" style="background:#0ea5e9">📁</button>
<button class="jd-icon" style="background:#8b5cf6">⚙️</button>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0b0f1a; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.jd-stage { display: flex; flex-direction: column; align-items: center; gap: 32px; padding: 24px; }
.jd-hint { font-size: 13px; color: #64748b; text-align: center; max-width: 380px; }
.jd-dock {
display: flex;
gap: 16px;
padding: 14px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 22px;
backdrop-filter: blur(12px);
}
.jd-icon {
width: 56px; height: 56px;
border-radius: 16px;
border: none;
font-size: 24px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
will-change: transform;
transform-origin: bottom center;
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
}// Each icon runs its own spring-damper simulation on scaleX/scaleY, anchored
// to transform-origin: bottom center, so hovering it looks like poking a soft
// rubber tile: it compresses downward on entry, then overshoots into a taller
// stretch before settling — real per-frame physics, not a fixed keyframe.
function attachJellySpring(el) {
var sx = 1, sy = 1, vx = 0, vy = 0;
var targetSx = 1, targetSy = 1;
var stiffness = 0.22, damping = 0.68;
var running = false;
function frame() {
var fx = (targetSx - sx) * stiffness;
var fy = (targetSy - sy) * stiffness;
vx = (vx + fx) * damping;
vy = (vy + fy) * damping;
sx += vx;
sy += vy;
el.style.transform = 'scale(' + sx.toFixed(4) + ',' + sy.toFixed(4) + ')';
var settled = Math.abs(vx) < 0.0008 && Math.abs(vy) < 0.0008 &&
Math.abs(targetSx - sx) < 0.0015 && Math.abs(targetSy - sy) < 0.0015;
if (settled) {
sx = targetSx; sy = targetSy;
el.style.transform = 'scale(' + sx + ',' + sy + ')';
running = false;
return;
}
requestAnimationFrame(frame);
}
function wake() { if (!running) { running = true; requestAnimationFrame(frame); } }
el.addEventListener('mouseenter', function () {
targetSx = 1.22; targetSy = 0.74; // quick compress, wider footprint
wake();
setTimeout(function () {
// Rebound into a taller stretch before finally settling at 1,1.
targetSx = 0.92; targetSy = 1.28;
wake();
setTimeout(function () { targetSx = 1; targetSy = 1; wake(); }, 140);
}, 90);
});
el.addEventListener('mouseleave', function () {
targetSx = 1; targetSy = 1;
wake();
});
}
document.querySelectorAll('.jd-icon').forEach(attachJellySpring);Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Jelly Icon Dock Hover — Bottom-Anchored Spring Squash-Rebound-Settle Sequence
This dock treats each icon like a soft rubber tile sitting on a surface: hovering one compresses it downward, then it rebounds into a taller stretch before finally settling back to normal size — a three-stage physical sequence, not a single squash-and-return like jelly press button, and not a rigid scale-up like a typical macOS-style dock magnification effect. Each icon runs its own independent spring simulation, so hovering across the row shows several icons at different points in their own compress-rebound-settle cycle at once.
Bottom-anchored transform origin
Every .jd-icon has transform-origin: bottom center. This single CSS property is what makes the squash read as "sitting on a surface" rather than floating in space — when scaleY shrinks, the icon's bottom edge stays anchored and only its top compresses downward, exactly like pressing down on a rubber tile fixed to a shelf, rather than shrinking symmetrically from its own center.
The three-stage sequence
On mouseenter, the target scale first becomes { sx: 1.22, sy: 0.74 } — wide and flat, the compression. A setTimeout of 90ms later, the target flips to { sx: 0.92, sy: 1.28 } — narrow and tall, the rebound stretch, simulating the stored energy from the compression releasing upward. A further 140ms later, the target settles at { sx: 1, sy: 1 }, natural size. Each stage change calls wake() to restart the physics loop if it had gone idle, and because the spring's own velocity carries over between target changes, the transitions between stages blend smoothly rather than snapping.
The spring-damper loop itself
Identical in structure to jelly press button's physics: each frame computes a force toward the current target ((target - current) * stiffness), accumulates it into velocity, damps that velocity, and integrates it into the current scale. The loop self-terminates once velocity and distance-to-target both fall under a small epsilon, avoiding wasted requestAnimationFrame calls once an icon is visually at rest.
Per-icon independence
attachJellySpring(el) creates a fresh closure over its own sx, sy, vx, vy, and running flag for every icon it's called on. There's no shared state between icons — hovering one doesn't affect any other icon's spring, and rapidly hovering several icons in sequence lets each one run its full three-stage sequence independently and concurrently.
Interrupted hovers
If the cursor leaves an icon mid-sequence, mouseleave immediately overrides the target back to { 1, 1 } — because the spring loop always chases whatever the current target is, this cleanly interrupts an in-progress compress or rebound stage rather than needing to cancel and restart a separate timeline.
Step by step
How to Use
- 1Hover any dock iconWatch the three-stage motion: a quick downward compression, a rebound into a taller stretch, then a settle back to normal size.
- 2Hover multiple icons quicklySweep across the dock — each icon runs its own independent sequence, so several can be mid-motion simultaneously.
- 3Move away mid-sequenceLeave an icon partway through its rebound and watch the spring smoothly redirect back toward normal size instead of finishing the stretch first.
- 4Tune the timingIn the JS panel, adjust the 90ms and 140ms setTimeout delays to change how quickly each stage of the sequence transitions.
- 5Tune the physics feelChange stiffness and damping at the top of attachJellySpring for a snappier or bouncier overall feel.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The sequence models a physical compression-and-release: the icon first compresses under an implied "poke" (wide and flat), then the stored energy rebounds it into a taller stretch, before settling to rest. This three-stage motion reads as far more physical and tactile than an instant scale-up.
Without it, scaling shrinks and grows symmetrically from the icon's own center, which looks like it is floating and expanding in place. Anchoring at the bottom edge keeps the icon's base fixed, so only the top visibly compresses and stretches — matching how a real tile pressed on a surface would deform.
Each call to attachJellySpring(el) creates its own private sx, sy, vx, vy, and running variables inside a closure. There is no shared state between icons, so hovering several icons in quick succession lets each run its own full sequence independently and concurrently.
The mouseleave listener immediately sets the target back to {1, 1}. Because the spring loop always chases whatever the current target is (using its existing velocity), this smoothly redirects the icon back to rest from wherever it currently is in its sequence, rather than snapping or requiring the rebound stage to finish first.
Adjust the target values inside the mouseenter handler: {sx: 1.22, sy: 0.74} for the compression stage and {sx: 0.92, sy: 1.28} for the rebound stage. Values closer to 1 on both axes produce a subtler effect; further from 1 produces a more exaggerated cartoon-like jelly motion.
Yes. Click "JSX" for a React component. Keep the spring state (sx, sy, vx, vy, running, and any pending timeout ids) in a ref per icon rather than component state, since it updates on every animation frame, and clear any pending setTimeout calls on unmount to avoid memory leaks.