Spring Physics
Each box stores its own spring physics: a click imparts an initial force, the loop calculates acceleration and damping frame by frame until the box returns to rest. Several boxes bounce independently on the same page, each with its own cycle. No CSS animation, no library.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Hover has 48 effects, including 5 free. Explore the category →
3 usage examples



How it works
The .fx-0386-root wrapper centres the three .spring-box[data-spring] boxes horizontally in a 300 px tall container. The script finds every root on the page — multiple instances coexist — and for each data-spring box creates an isolated physics context: a displacement t, an acceleration a, and a running flag.
On click (or touch), a receives an impulse of −15. Each frame the loop applies a += 0.3 × −t (restoring force proportional to displacement, Hooke's law), then a *= 0.7 (30 % damping) and t += a. The box is repositioned via translateY(t) scale(1 + 0.01 × |t|): it rises, overshoots, and returns with decreasing oscillations. When |t| < 0.05 and |a| < 0.05, the loop stops and the box snaps back to zero.
Each box has its own requestAnimationFrame, independent of the others: clicking « BOUNCE » while « SPRING » is still bouncing does not interfere with either cycle. Two copies of the effect on the same page work identically, each found by its own .fx-0386-root.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the loop does not start and CSS forcestransform: noneon the boxes. The boxes remain visible, clickable, and readable — the visual bounce disappears, not the content. - Keyboard:
.spring-boxelements are clickable<div>elements but not focusable by default — addtabindex="0"and akeydownlistener for Enter/Space to make them keyboard-accessible when used as functional buttons. - Touch: a
touchstartevent triggers the spring, withpreventDefaultto avoid double-firing mouse/touch. The effect works on mobile touchscreens. - Decorative or functional role: if the boxes carry an action (vote, download), replace the
<div>elements with<button>elements and remove the redundanttouchstartlistener — a<button>responds natively to keyboard and touch events.
Browser compatibility
ES5 JavaScript (closures, requestAnimationFrame, matchMedia), no dependency. Not measured in Firefox and WebKit: the expected behaviour is identical since the effect uses only universal APIs.
Without JavaScript, the boxes display at their rest position with no animation. Without matchMedia (very old browsers), the spring animates even under reduced motion.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0386-root"><div class="physics-container"><div class="spring-demo"><div class="spring-box" data-spring="">LABEL</div></div><span class="info-label">Help text</span></div></div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Initial impulse (JS — a = -15) |
−15 | −8 gives a softer spring, −25 a stronger bounce. Negative goes down first, positive goes up first. |
Restoring force (JS — a += 0.3 × −t) |
0.3 | 0.1 slows the spring (long oscillations), 0.5 speeds it up (abrupt return). |
Damping (JS — a *= 0.7) |
0.7 | 0.9 gives an almost frictionless spring (bounces for a long time), 0.5 stops it quickly. |
Box colour (CSS — background of .spring-box) |
linear-gradient(135deg, #6366f1, #8b5cf6) |
Replace with your brand's primary colour or a solid fill. The box labels can be changed directly in the HTML. |
| Number of boxes (HTML) | 3 boxes | Add or remove .spring-box[data-spring] elements; each new box automatically receives its own physics. |
FAQ
requestAnimationFrame loop. Clicking all three quickly launches three springs in parallel without interference.|t| < 0.05 and |a| < 0.05). To speed up the stop, lower the damping value (for example a *= 0.5).click listener with mouseenter, and add a = 0; t = 0; on mouseleave if you want to reset the state on exit.