Repel Particles
100 DOM particles flee the cursor within an 80 px radius with proportional force, then smoothly interpolate back to their origin.
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
On load, the script creates 100 <div class="rpe-particle"> elements inside .rpe-zone and assigns each a random position (left, top in percentage). Each particle stores its origin coordinates. The particles are DOM elements, not a canvas: each can be styled individually in CSS.
A mousemove listener on the zone updates the cursor position. The requestAnimationFrame loop computes the Euclidean distance from each particle to the cursor. If that distance is under 80 px, the particle is repelled with a force inversely proportional to the distance (force = (100 − dist) / 100 × maxForce); its temporary position is offset in the direction away from the cursor. Outside the radius, the position interpolates back to the origin: x += (origX − x) × 0.05 per frame. An IntersectionObserver stops the loop when the zone leaves the screen.
Under prefers-reduced-motion: reduce, the loop does not start; particles stay at their initial position, the background remains visible and static. On touch screens, touchmove updates the virtual cursor position so particles flee the finger too.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the animation loop does not start. Particles stay at their initial position: the background remains decorative but static. - Decorative role: particles convey no information; they are implicitly hidden from assistive technologies by their empty content. No text or functional content should be placed inside particles.
- Touch: the
touchmoveevent on the zone updates the virtual cursor position. Particles flee the finger on touchscreens too. - No critical interaction: particle fleeing is purely decorative. No user action should depend on particle positions — they are not buttons or targets.
Browser compatibility
DOM + requestAnimationFrame + 2.2 KB of JavaScript, no dependency. 100 DOM elements transformed per frame: measured at under 2 ms of JS per frame in Chromium on desktop. On low-end mobile, reduce to 50 particles if animation stutters. IntersectionObserver available since Chrome 51, Firefox 55 and Safari 12.1. Measured in Chromium 151, Firefox 153 and WebKit 26.
Without JavaScript, the 100 divs are not created; the .rpe-zone area renders empty. Without IntersectionObserver, the loop also runs off screen — CPU impact proportional to the particle count.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0372-root">
<div class="rpe-zone">
<span class="rpe-label" aria-hidden="true">Move your mouse</span>
<!-- The 100 div.rpe-particle elements are created by JS -->
</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 |
|---|---|---|
Number of particles (JS — count = 100) |
100 particles | Lower to 50 for a lighter effect; raise to 150 for a denser background. Avoid more than 200 on low-end mobile. |
Flee radius (JS — if (dist < 80)) |
80 px | Raise to 120-160 px for a reaction as soon as the cursor gets close; lower to 40 px for a subtler effect. |
Max force (JS — maxForce) |
40 px | Raise to 80-80 px for particles that flee far; lower to 15-20 px for a discreet shift. |
Return speed (JS — lerp = 0.05) |
0.05 (5% per frame) | 0.02 gives a slower, smoother return; 0.15 gives a snappy return. At 1, particles return instantly. |
Particle appearance (CSS — .rpe-particle) |
white 3×3 px square, opacity 0.7, border-radius 50% | Change size, colour, shape (remove border-radius for squares) or add an emoji as the particle div's HTML content. |
FAQ
.rpe-particle CSS (or add variant classes) or inject a character into the divs when creating them: p.textContent = '★' inside the creation loop.touchmove event updates the virtual cursor position by extracting touches[0].clientX/Y. Particles flee the finger just as they flee the mouse.