Buttons✨ Premium

Magnetic Button

The button slides toward the cursor (30% of its offset from center) and snaps back in 300 ms — a CTA that answers the hand before the click.

JSMouseInteractive

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Buttons has 27 effects, including 2 free. Explore the category →

3 usage examples

Free-trial button — SaaS pricing card — Magnetic Button example 1

① Free-trial button — SaaS pricing card

WhenThe user compares plans on a Pricing page; each card ends with a commitment button.
WhyThe movement singles out the CTA from the rest of the card (price, feature list) without an extra color: it says “this is where you click” as soon as the hand gets close.
SettingsCoefficient 0.3 → 0.2 for a dense grid (neighboring buttons don't overlap), transition: transform .25s, button at width: 100%.
Primary CTA — landing page hero — Magnetic Button example 2

② Primary CTA — landing page hero

WhenA single primary button in the center of the hero, next to a static secondary link.
WhyOn a large button the travel reaches ±35 px and becomes the page's first visible interaction; the secondary link, which stays put, acts as a reference point.
SettingsCoefficient 0.3 kept, transition: transform .35s for a heavier follow, padding: 18px 40px, 17 px / 700 text.
Contact band — agency or freelance site — Magnetic Button example 3

③ Contact band — agency or freelance site

WhenAt the bottom of the page, a “Got a project? Let's talk” band with a single button.
WhyOn a flat background with no other interactive element the pull is clearly perceived; it's the classic agency-site pattern, and a pill-shaped button suits it.
SettingsCoefficient 0.4 (bolder travel, ±40 px on a 200 px button), transition: transform .2s, border-radius: 999px.

How it works

The script attaches a mousemove listener to every element carrying the data-magnetic attribute. On each event, getBoundingClientRect() returns the button's position and size, then two offsets are computed: a = clientX − left − width/2 and l = clientY − top − height/2 — the cursor position relative to the center, in pixels.

The shift is written as an inline style: transform: translate(0.3·a px, 0.3·l px). The button therefore travels 30% of the way toward the cursor (slightly less in practice: the rect is re-read on the already-shifted button, so the measured offset is shorter). Maximum travel is 0.3 × half-size: ±30 px horizontally and ±8 px vertically for a 200 × 52 px button.

Smoothing is pure CSS: transition: transform .3s on .btn-magnetic (default ease curve). Each new target is reached in 300 ms while the cursor moves faster — hence the feeling of a button being pulled, lagging behind the hand. On mouseleave the script writes translate(0, 0) and the same transition brings the button home.

Selecting by attribute (querySelectorAll('[data-magnetic]')) makes the effect multi-instance with no ids: every marked button is wired in one pass. The listener sits on the button itself, not on a zone around it: attraction only starts once the cursor touches the button (see the FAQ for a wider attraction zone).

Accessibility

  • prefers-reduced-motion not handled: the button moves on every hover regardless of the system preference. Add @media (prefers-reduced-motion: reduce) { .btn-magnetic { transition: none; transform: none !important } } — the !important is required because the JS writes the transform inline — or test matchMedia('(prefers-reduced-motion: reduce)').matches before attaching the listeners.
  • The effect is mouse-only (mousemove / mouseleave). With keyboard or touch, the button is a plain native <button>: nothing is lost, the movement is decoration.
  • Insufficient contrast as shipped: white text on the cyan → blue gradient measures 2.4:1 on the #06b6d4 end and 3.7:1 on the #3b82f6 end, below the 4.5:1 AA threshold for 16 px text. Darken the gradient (#0e7490 → #1d4ed8 gives 5.4:1 and 6.7:1) or set the label at 18 px bold (3:1 threshold).
  • The code sets border: none but leaves outline alone: the native focus ring stays visible when tabbing. Don't remove it; if you restyle it, use :focus-visible with a contrasting 2 px ring.
  • Touch target ≈ 52 px tall (16 px padding + 16 px text), above the 44 px minimum. Screen readers: native <button> with a text label, no ARIA needed; the inline transform doesn't alter the accessibility tree.

Browser compatibility

Requires getBoundingClientRect, ES2015 arrow functions and template literals, and a CSS transform transition — every browser since 2016. Zero dependencies.

Chrome 49+✓ Full
Firefox 45+✓ Full
Safari 10+✓ Full
Edge 79+✓ Full
Mobile iOS✓ Static (no hover)
Android Chrome✓ Static (no hover)

Without JavaScript, the button renders and works normally, gradient and rounded corners included; only the movement is gone. No error if the page has no data-magnetic element: the forEach loop is simply empty.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<button class="demo-btn btn-magnetic" data-magnetic>Magnetic</button>
Unlock the full code — $9.90/mo incl. tax

The full HTML + CSS + JS of Magnetic Button, ready to paste — plus all 840 effects. Or $299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Coefficient .3 (JS — translate(${.3*a}px, ${.3*l}px)) .3 Fraction of the cursor/center offset applied to the button. 0.15 = a quiver; 0.5 = the button travels half the way and visibly leaves its slot.
transition: transform .3s (CSS — .btn-magnetic) .3s ease Follow latency. .1s sticks to the cursor (twitchy); .5s gives a heavy motion. The return on mouseleave uses the same duration.
[data-magnetic] selector (JS) valueless attribute Put the attribute on any element (link, card, icon) to magnetize it — the script assumes nothing about the tag. Remove it to disable one button without touching the JS.
background (CSS — .btn-magnetic) linear-gradient(135deg, #06b6d4, #3b82f6) Cyan → blue gradient. Swap in your colors; for AA contrast with white text, stay on dark shades (#0e7490 → #1d4ed8).
padding / font-size (CSS — .demo-btn) 16px 32px / 1rem Button size — hence maximum travel, computed from width/2 and height/2. A button twice as wide moves twice as far.
border-radius (CSS — .demo-btn) 12px Corner shape. 999px gives a pill; the movement doesn't depend on the shape.
Rest state (JS — mouseleave) translate(0, 0) Write translate(0, 0) scale(1) here and add scale(1.05) to the mousemove string to combine attraction with a slight zoom.

FAQ

Not with the code as shipped: mousemove is listened to on the button itself, so the effect starts on contact. For an attraction zone, wrap the button in a larger <div> (e.g. padding: 40px), put data-magnetic on that parent, and apply the transform to the child: replace e.style.transform with e.firstElementChild.style.transform in both listeners.
The script runs once at load: buttons mounted later aren't wired. Move the two addEventListener calls into a useEffect / onMounted with a ref to the button, and remove them on unmount (removeEventListener). The math stays the same.
No: transform is outside the layout flow, neighbors don't move and no reflow is triggered on them. Just leave a margin at least equal to the maximum travel (0.3 × half-width) so the button never overlaps a neighbor, and avoid a parent with overflow: hidden that would clip it.