Cursors✨ Premium

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.

JSInteractive

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

Primary hero CTA — SaaS landing page — Magnetic Effect example 1

① Primary hero CTA — SaaS landing page

WhenA landing page bets everything on a single button ('Book a demo') and wants it impossible to miss without making it blink.
WhyThe attraction starts 150 px from the center: the button moves toward the cursor before being hovered, which enlarges its effective target (Fitts's law). The 30% cap keeps the motion discreet — craft, not gimmick.
Settings120 px radius and 0.2 coefficient to stay believable on a button; .25s transition; attach the listener to the hero container, not the button, to keep the at-a-distance onset.
Footer social icons — floating dock — Magnetic Effect example 2

② Footer social icons — floating dock

WhenA row of 40–48 px icons (GitHub, LinkedIn, X…) in a footer or a floating bar — small targets sitting close to each other.
WhyOn small targets, the magnetism corrects imprecise gestures: the icon shifts toward the cursor and the click lands cleanly. This is the one case where a high coefficient is justified — the absolute displacement stays small because the element is small.
SettingsRadius shrunk to 70–80 px so two neighboring icons never fight over the cursor; coefficient raised to 0.4; .2s transition; one zone/target pair per icon.
Project cards in a portfolio grid — agency site — Magnetic Effect example 3

③ Project cards in a portfolio grid — agency site

WhenA grid of project thumbnails where each card should react as the cursor approaches, inviting the click without turning the page into a funfair.
WhyOn a large surface, a few pixels are enough to create a sense of material that responds: the card 'breathes' toward the visitor, and the elastic return on mouseleave makes it settle back on its own.
SettingsCoefficient 0.12–0.15 — a large card should move less than a small button; 200 px radius to match the card's scale; .4s transition for weighty motion; space your grid accordingly, a translated card can overlap its neighbor.

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, mousemove never 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-visible state (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.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 14+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Inert (no cursor)
Android Chrome✓ Inert (no cursor)

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

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
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

The code targets two unique 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.
Two reasons. The at-a-distance onset: attached to .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.
No — the smoothness does not come from the 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.