Hover✨ Premium

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.

JSElasticMouse

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

Bass strings — music studio app — Elastic String example 1

① Bass strings — music studio app

WhenIn Quelvo, three coloured strings (E, A, D) appear in the instrument panel. The musician hovers over each string to pluck it; the string curves toward the pointer then springs back oscillating.
WhyThe damped spring physics faithfully replicates the feel of a string pluck: an immediate amplitude peak then a natural decay. Direct interaction on each string makes the instrument demo intuitive and tactile.
SettingsThree .fx-0382-root instances stacked. Nodes styled as horizontal lines (height: 3/2/1 px, border-radius:1px, flex:1) to look like real strings. Colours E=red, A=orange, D=green. Wrap height 100%, prefix k382-.
Living section divider — creative portfolio — Elastic String example 2

② Living section divider — creative portfolio

WhenBetween the Work and About sections of a portfolio, the string stretches toward the cursor as it passes. It subtly invites continued reading without interrupting the scroll.
WhyA static divider goes unnoticed; a reactive string turns a neutral element into an interaction moment. The visitor's attention is held for a fraction of a second, creating rhythm in the reading.
SettingsWrap height 40 px. Nodes as a line (height:1px, background:#334155). The string spans the full width between sections. No visible label.
Waveform scrubber — web audio player — Elastic String example 3

③ Waveform scrubber — web audio player

WhenIn Trellox, the elastic string replaces the classic progress bar. The visitor hovers over the waveform: the string deforms toward the pointer, indicating the target position in the track. The 20 blue-violet gradient nodes make the timeline feel alive.
WhyA flat bar feels passive; a string that reacts to hover turns the scrubber into a sensory interaction point. The local deflection around the cursor naturally communicates pointing precision.
SettingsWrap height 110 px. Default nodes (20 px, blue-violet gradient). The string spans the full player width between header and time markers. Prefix k382-s3-wave.

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: touchmove moves the string toward the finger just like the mouse. preventDefault() blocks page scrolling during the interaction. touchend lets 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 at translateY(0) — the string is visible as a fixed horizontal line.
  • Decorative role: .es382-wrap has no ARIA role. If the string serves as a section separator, add role="separator" and an aria-label describing 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.

Chrome 51+✓ Full (measured)
Firefox 55+✓ Full (measured)
Safari 12.1+✓ Expected full (not measured)
Edge 79+✓ Full (measured)
Mobile iOS 12.2+✓ Full on touch (not measured)
Android Chrome 51+✓ Full on touch (not measured)

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):

index.html — structure
<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 / propertyDefaultEffect
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

Yes. Access the internal node array and assign a velocity: 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.
The 20 divs are rectangles; visible corners cause a slight pixellation effect when the curve is steep. Increase the node count to 30-40 to reduce it, or replace the div elements with a canvas and draw the string with bezierCurveTo.
Add a clamp on y after the calculation: if (node.y > 80) node.y = 80;. Do the same upward (if (node.y < -80)) for a symmetric amplitude.