Magnetic Effect
A block pulled by the cursor as soon as it enters its 150 px radius: distance recomputed on every mousemove, a linear force capped at 30%, and a 300 ms CSS transition that makes the motion feel magnetic.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →
3 usage examples



How it works
The mousemove listener sits on the parent zone .cursor-zone (300 px tall), not on the .magnetic-target itself — the detail that makes it all work: the attraction kicks in before the cursor ever touches the block. On every event, the code re-reads the target's geometry with getBoundingClientRect(), derives its center, then measures the Euclidean distance (Math.sqrt) between that center and the pointer (clientX / clientY).
Everything then hinges on a 150 px threshold. Beyond it, the transform is reset to translate(0, 0). Inside it, a linear force is computed: (150 − distance) / 150 — 0 at the edge of the radius, 1 at the center. The applied offset is that force multiplied by the gap on each axis and by a 0.3 coefficient: the target never travels more than 30% of the distance separating it from the cursor — it leans toward it without catching up. And since getBoundingClientRect() is re-read on the already-displaced element, the math adjusts on every event: the motion self-stabilizes.
The JavaScript animates nothing: it writes raw values into transform: translate(…) on every mousemove. The CSS rule transition: transform .3s is what interpolates between two successive writes and manufactures the 'magnetic' inertia — as well as the elastic return when mouseleave resets the transform to zero. No requestAnimationFrame, no hand-rolled lerp: translate() is GPU-composited, no reflow.
Accessibility
- prefers-reduced-motion: absent from the code. The listeners write transforms regardless of the system preference. Gate their registration behind
window.matchMedia('(prefers-reduced-motion: reduce)')before deploying. - Under a finger,
mousemovenever fires: on touch screens the target simply stays put — a sound fallback. So never make the magnetic displacement the only cue that an element is clickable: part of your audience will never see it. - The attraction is invisible to the keyboard: if you turn the target into a button or a link, Tab navigation gets no equivalent of the pull. Provide a strong
:focus-visiblestate (ring, shadow) independent of the effect. Good news: the hit area follows the transform, so the displaced element stays clickable exactly where it renders. - The target is a
<div>with no role and no meaningful text — purely decorative as shipped. For interactive use, start from a real<button>or<a>: the transform does not alter the accessibility tree, screen readers announce nothing about the motion, so the semantics must come from the element itself.
Browser compatibility
On the CSS side, transform: translate() and transitions have been universal since 2013. The real floor is the JavaScript: the code uses arrow functions and a template literal (ES2015) — IE 11 halts on a syntax error before the first mousemove.
The fallback is built into the design: without JavaScript, on an ES5-only engine, or on a touch screen, no transform is ever written and the target stays centered and fully functional — the displacement is purely additive, the layout never depends on it. If you truly must support an ES5 engine, transpile the script: the logic has no other dependency.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| Activation radius (150 in the JS) | 150 px | The value 150 appears twice: in the condition distance < 150 and in the force formula (150 - distance) / 150. Change both together, otherwise the force no longer fades to zero at the edge of the radius and entering the zone produces a jolt. |
| Attraction coefficient (0.3) | 0.3 | Final multiplier of the translate: the share of the path toward the cursor the target agrees to travel. 0.1 = a discreet quiver; 0.5 = the block almost sticks to the pointer. Beyond that, the target can leave its zone — the container's overflow: hidden will clip it. |
| Transition duration (transform .3s) | .3s | It smooths the JS's successive writes — and sets the speed of the return on mouseleave. .15s = a nervous response; .5s = a viscous trail. |
| Transition curve | ease (implicit) | The shorthand transition: transform .3s names no timing function, so ease applies. Add cubic-bezier(0.34, 1.56, 0.64, 1) so the return slightly overshoots its resting position before settling — a genuine magnet snap. |
| Target dimensions (120 × 120) | 120 px × 120 px | The geometry is re-read on every event via getBoundingClientRect(): change width and height freely, the center is recomputed on its own — unlike the radius, which is hard-coded in the JS. |
| Listening zone (.cursor-zone) | 300 px tall | This container carries the mousemove listener, not the target. Widen it so the magnetism starts from farther away; smaller than the 150 px radius, it makes the threshold pointless — the effect becomes active everywhere in the zone. |
FAQ
ids via getElementById — a single zone/target pair. Switch to classes with querySelectorAll and loop, recreating the listener/target pair for each: since the math is relative to each target's center, nothing else changes. If several targets share one zone (an icon dock), compute each one's distance inside the same listener and only magnetize the ones under the threshold..magnetic-target, mousemove would only fire on direct hover — the 150 px attraction would vanish. Stability: the target moves underneath the cursor, so listening to it directly would produce cascading enter/leave cycles. The parent never moves: the event stream stays continuous.mousemove rate but from transition: transform .3s, which interpolates between two successive writes. If the result feels choppy, lengthen the transition duration before reaching for requestAnimationFrame. A per-frame lerp is only warranted for continuous 1:1 cursor tracking — that is a different effect.