Cursors✨ Premium

Magnetic Cursor

A button that leans toward your cursor for as long as you hover it: the JS measures the offset from center, writes it into two CSS variables, and :hover decides when the translation applies.

JSCSS Variables

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 or product landing page — Magnetic Cursor example 1

① Primary hero CTA — SaaS or product landing page

WhenThe main action button ('Start free trial', 'Book a demo') stands alone in the hero and deserves a micro-interaction that sets it apart from secondary buttons.
WhyThe lean only triggers when the button itself is hovered: it rewards intent that is already formed instead of soliciting it from afar. The button seems to move toward the click — a responsiveness that builds product credibility without adding anything to the page.
SettingsCoefficient lowered to 0.2 for a wide button (the measured offset grows with element size); transition at 0.25 s for a crisp response.
Social icons in a footer or contact page — Magnetic Cursor example 2

② Social icons in a footer or contact page

WhenA row of small icons (GitHub, LinkedIn, X…) at 40–48 px, notoriously narrow click targets, that you want to make livelier and easier to aim for.
WhyOn a small element, the lean acts as aim assistance: the icon shifts in the cursor's direction and stays under it as the click approaches. The effect remains subtle, since the offset from center never exceeds half the icon.
SettingsCoefficient raised to 0.4 — on 44 px, travel stays under 9 px; transition shortened to 0.2 s; .magnetic-area wrapper duplicated around each icon using the querySelectorAll variant described in the FAQ.
Portfolio thumbnails or a project grid — Magnetic Cursor example 3

③ Portfolio thumbnails or a project grid

WhenA studio or freelancer showcases projects as a grid of clickable cards and wants an interaction signature that relies neither on zoom nor on drop shadows.
WhyA card leaning a few pixels toward the cursor feels mounted on a ball joint. As the translation follows the pointer's position inside the card, every hover zone produces a different posture — physical, not scripted.
SettingsCoefficient reduced to 0.08–0.12: on a 300 px card, the offset from center reaches 150 px and 0.3 would yield 45 px of travel — far too much; transition extended to 0.4 s to add weight; :hover kept as the sole trigger.

How it works

The effect relies on a strict separation of roles. The button is wrapped in a .magnetic-area container, and that container — not the button — is what moves. On the CSS side, the key rule fits on one line: .magnetic-area:hover receives transform: translate(var(--magnetic-x, 0), var(--magnetic-y, 0)). The translation is driven by two CSS variables with a fallback of 0, and it is only declared under :hover: outside hover, no transform exists. A transition: transform .3s on the container smooths both the pursuit and the return to rest.

On the JavaScript side, the mousemove listener sits on the container itself, not on the zone around it. On every move, getBoundingClientRect() returns the element's actual position; the script derives the pointer's offset from its center (clientX - rect.left - rect.width / 2, same for Y), multiplies it by 0.3 and writes the result into --magnetic-x and --magnetic-y through style.setProperty(). The JS never touches transform: it supplies numbers, the CSS decides how to apply them. A second listener, mouseleave, resets both variables to 0px. The whole thing fits in about fifteen lines, with no requestAnimationFrame and no library.

This architecture produces an emergent behavior: since getBoundingClientRect() measures the already-transformed position, each mousemove computes the offset from where the element currently sits. The pull is self-damping: the element converges under the pointer instead of oscillating, and settles around 23% of the initial offset (0.3 / 1.3), not the coefficient's 30%. Another consequence: without JavaScript, the variables are never written, the 0 fallback kicks in and the button remains fully functional — the effect is progressive enhancement, not a dependency.

Accessibility

  • prefers-reduced-motion: absent from the code. Add @media (prefers-reduced-motion: reduce) { .magnetic-area:hover { transform: none; } } — a single rule neutralizes the effect, since the entire translation goes through that selector.
  • The effect is 100% mouse-driven: mousemove fires neither from the keyboard nor from touch. The native <button> stays focusable and activatable with Enter/Space, but no :focus-visible style is defined — add one, keyboard users will never see the magnetic lean.
  • On coarse pointers (touch), no mousemove flows in normal use: the effect is inert, the button behaves normally. To block the emulated mousemove some browsers fire on tap, gate the listener behind matchMedia('(hover: hover) and (pointer: fine)').
  • For screen readers nothing changes: the movement is purely visual, and the <button> keeps its accessible name and its place in the accessibility tree. Never carry information through the movement itself — it is invisible to part of your users.

Browser compatibility

Relies on CSS variables (var() written through setProperty()), transitions and getBoundingClientRect(). The limiting factor is custom property support — universal today, absent from IE 11.

Chrome 49+✓ Full
Firefox 31+✓ Full
Safari 9.1+✓ Full
Edge 15+✓ Full
Mobile iOS✓ Inert (touch)
Android Chrome✓ Inert (touch)

Degradation is natural: without CSS variables (IE 11), <code>translate(var(…))</code> is invalid, the browser drops the declaration and the button renders static but fully clickable. Without JavaScript, the variables are never written and the 0 fallback in <code>var(--magnetic-x, 0)</code> guarantees a zero translation. No fallback code to write.

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
Attraction coefficient (x * 0.3 / y * 0.3 in the JS) 0.3 The two multiplications set the amplitude: the value written into the variables equals 30% of the offset from center. 0.1 = a barely perceptible quiver; 0.5 = pronounced pursuit. Edit both occurrences together to keep the movement isotropic.
transition: transform .3s (on .magnetic-area) .3s Smooths every new variable value and animates the return to rest after mouseleave. 0.15 s = snappy response, glued to the cursor; 0.5 s = visible lag, the button feels heavy. It is the effect's only speed control.
Return curve (timing-function) ease (implicit) The shorthand keeps the default ease curve. Switch to cubic-bezier(0.34, 1.56, 0.64, 1) for an elastic return that slightly overshoots the rest position — the 'magnet release' feel typical of this kind of interaction.
The :hover gate (.magnetic-area:hover) translation applied on hover only Declaring the transform under :hover is a safety net: outside hover, no translation can persist. Move the declaration onto .magnetic-area alone if you drive the variables some other way — the return to rest will then depend solely on the JS mouseleave.
Asymmetric X / Y coefficients 0.3 on both axes Nothing forces the same factor on both axes: y * 0.15 with x * 0.3 constrains the vertical and produces a lateral glide — relevant for a wide, short button, where the horizontal offset largely dominates the vertical one.
The wrapper's content (.magnetic-btn) a gradient-styled <button> .magnetic-area carries the listener, the transition and the translation — the button is just a passenger. Swap it for a link, an SVG icon or an entire card: as long as the element sits inside the wrapper, the mechanics work without touching the JavaScript.

FAQ

'Magnetic Effect' creates an attraction field: it listens on the whole surrounding zone, computes the Euclidean distance to the target's center and applies a force that fades within a 150 px radius — the element moves toward the cursor before it is even hovered. 'Magnetic Cursor' does the opposite: the listener sits on the element itself, with no radius and no distance math, and the lean only exists during hover, locked behind :hover. One attracts from afar, the other accompanies on contact. And in the code: the former writes the transform inline from JS; here the JS only writes two variables, the CSS stays in charge.
The code targets a single id through getElementById — one instance only. To equip several, switch to the class: iterate over document.querySelectorAll('.magnetic-area') and attach the mousemove/mouseleave pair to each element in the loop. Since the variables are written on each wrapper's inline style, instances are independent.
Nothing — and that is consistent with the mechanics: without a mouse, no mousemove flows, the variables stay at their 0 fallback and the button behaves normally. No need to disable anything. To tell stylus or trackpad apart, the (hover: hover) and (pointer: fine) media query remains the right criterion.