Cloth Simulation
A canvas of 20×15 nodes linked by distance constraints simulates a soft textile: top-row nodes are pinned, the rest fall under gravity and react to the cursor within a 50 px radius. Sharp on Retina screens, loop paused off-screen, static cloth under reduced motion.
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-0388-root wrapper contains a .fx-0388-canvas that is 100 % wide by 300 px tall. At init, the script measures the canvas width, applies devicePixelRatio for the internal resolution, and builds a 15 × 20 = 300-node grid with nodes spaced 15 px apart. The 20 top-row nodes are pinned (pinned: true); the 280 others are free.
Each frame, for each free node: gravity adds 0.2 px/frame downward, the cursor (or finger) within a 50 px radius adds an attraction force proportional to the gap, and elasticity pulls each node back toward its rest position. Velocities are damped by 0.98 per frame. Everything is scaled by dt / 16.67 (elapsed time capped at 50 ms) so the simulation stays stable after a background tab.
An IntersectionObserver watches the root: the loop stops off-screen. Under reduced motion, nodes are drawn in a static grid with no loop. mousemove and touchmove events position the cursor in CSS coordinates (the canvas being in CSS pixels thanks to ctx.scale(dpr, dpr)).
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the loop does not start and the cloth is drawn as a static grid at its initial positions. The effect is visible and readable. - Touch:
touch-action: noneon the canvas prevents page scrolling when a finger slides over it. The effect responds to touch on mobile. If the canvas fills the full width of a mobile page, considertouch-action: pan-yto preserve vertical scrolling. - Decorative role: the canvas carries no text or ARIA role. If the cloth represents a product or a flag, expose the name as text nearby.
- Keyboard: no keyboard interaction is built in — the effect is designed for mouse and touch.
Browser compatibility
Canvas 2D (devicePixelRatio, IntersectionObserver, ResizeObserver, matchMedia), no dependency. IntersectionObserver available since Chrome 51, Firefox 55 and Safari 12.1; ResizeObserver since Chrome 64, Firefox 69 and Safari 13.1. Not measured in Firefox and WebKit.
Without JavaScript, the canvas stays empty. Without ResizeObserver, the canvas size does not adjust if the frame changes width. Without IntersectionObserver, the loop runs off-screen too.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0388-root"><div class="physics-container"><canvas class="fx-0388-canvas"></canvas><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 |
|---|---|---|
Thread colour (JS — ctx.strokeStyle = 'rgba(99,102,241,0.5)') |
rgba(99,102,241,0.5) (semi-transparent indigo) |
Replace with your textile colour. An alpha between 0.3 and 0.7 gives the grid transparency. |
Gravity (JS — n.vy += 0.2 * scale) |
0.2 px/frame | 0.05 gives an almost weightless cloth (space-like veil), 0.5 a heavy cloth that falls quickly. |
Cursor reaction radius (JS — Math.sqrt(dx*dx+dy*dy) < 50) |
50 px | 80 px for a broader, softer effect; 25 px for a more pinpoint reaction. |
Elastic restoring force (JS — n.vx += 0.01 * (n.ox - n.x) * scale) |
0.01 | 0.03 pulls the cloth back to rest faster (less persistent deformation). |
Grid resolution (JS — COLS = 20, ROWS = 15, SEG = 15) |
20×15 nodes spaced 15 px apart | Increase COLS and ROWS for a finer mesh (more CPU-intensive). Reduce SEG to tighten the nodes. |
FAQ
n.vx += 0.01 * (n.ox - n.x) pulls each node back toward its rest position. Damping (vx *= 0.98) slows the oscillations. Increase the restoring coefficient for a faster return.ctx.clearRect(0, 0, W, H). For a coloured background, add ctx.fillStyle = '#your-colour'; ctx.fillRect(0, 0, W, H); just before drawing the threads, and set the CSS background of .fx-0388-root to the same colour.