Elastic String
Twenty nodes form a horizontal string that bends toward the cursor when it comes close, then springs back oscillating like a damped spring — instant response to hover, motion that fades within seconds.
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 string is made of 20 div.es382-node elements created dynamically inside .es382-string. Each node has a vertical displacement y and a velocity vy. On every frame of the requestAnimationFrame loop, if the mouse is near the node (horizontal distance < 60 px), a force proportional to that proximity pushes the node toward the mouse: vy += (target − y) × 0.1 × f. The restoring force pulls each node back to its rest position (vy += −y × 0.3 × f). Damping (vy *= 0.9^f) makes oscillations decay. The final displacement is applied as translateY.
No Catmull-Rom smoothing or SVG filter is used here: the illusion of continuity comes from juxtaposing 20 small elements. Resolution can be increased by changing the node constant. An IntersectionObserver pauses the loop when off screen. The step is normalised to 60 fps: f = dt / 16.67.
Accessibility
- Touch:
touchmovemoves the string toward the finger just like the mouse.preventDefault()blocks page scrolling during the interaction.touchendlets the string oscillate freely. - Keyboard: the effect does not respond to the keyboard. There is nothing to select or activate; the element is purely decorative.
- Reduced motion: under
prefers-reduced-motion: reduce, the loop does not start and nodes remain attranslateY(0)— the string is visible as a fixed horizontal line. - Decorative role:
.es382-wraphas no ARIA role. If the string serves as a section separator, addrole="separator"and anaria-labeldescribing the two areas separated.
Browser compatibility
Pure DOM and CSS, no dependency. IntersectionObserver available since Chrome 51, Firefox 55, Safari 12.1. requestAnimationFrame since Chrome 10, Firefox 11, Safari 6. Not verified on real Safari; expected full.
Without JavaScript, the string appears as a fixed horizontal line. Without IntersectionObserver, the loop also runs off screen.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0382-root">
<div class="es382-wrap">
<div class="es382-track">
<div class="es382-string"></div>
</div>
<span class="es382-label" aria-hidden="true">Hover the string</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 |
|---|---|---|
Node count (JS — NODE_COUNT constant) |
20 | 30 gives finer resolution; 10 a discreet "beaded" look. Each node is a div: beyond 40, performance on mobile may drop. |
Spring stiffness (JS — vy += -y * 0.3 * f) |
0.3 | 0.5 gives a stiffer string that returns quickly; 0.08 a very soft string that oscillates for a long time. |
Damping (JS — vy *= Math.pow(0.9, f)) |
0.9 | 0.95 gives oscillations lasting several seconds; 0.7 an almost immediate return with no visible bounce. |
Thickness and colour (CSS — .es382-node) |
2 px tall, indigo-violet gradient | 4 px for a visible cable; a solid hue for a wire look. |
Mouse influence radius (JS — if (Math.abs(dx) < 60)) |
60 px | 120 px creates a wider wave; 30 px a pinched, local effect. |
FAQ
nodes[10].vy = -20; creates a peak at the centre. For a propagating wave, assign decreasing velocities across a range of consecutive nodes with a delay.div elements with a canvas and draw the string with bezierCurveTo.y after the calculation: if (node.y > 80) node.y = 80;. Do the same upward (if (node.y < -80)) for a symmetric amplitude.