Magnetic Button

A button that leans toward the cursor as it approaches, then springs back.

← All motions

Magnetic Button

The effect is not really about the button moving. It is about the invisible hit area around it — the part that makes the cursor feel pulled in before it ever touches the edge. Most implementations miss this and end up with a button that only reacts once you are already on top of it.

trigger: cursor pointer events CSS transition 18 lines of JS

Live demo

Move your cursor slowly across the whole area, not just the button.

The dashed outline is the hit area (≈120px of padding). The button is only the visible part.

The prompt

Make the primary call-to-action button magnetic.

Behaviour:
- Wrap the button in a container with about 60px of invisible padding on all sides. Attach the pointer listeners to that wrapper, not to the button, so the effect begins before the cursor reaches the button.
- On pointermove, compute the cursor offset from the wrapper centre, multiply by 0.3, and write the result into two CSS custom properties (--mx, --my) on the button.
- The button uses transform: translate(var(--mx), var(--my)) with a 0.35s cubic-bezier(.22,1,.36,1) transition for the return, and a 0.08s linear transition while the pointer is inside.
- On pointerleave, reset both custom properties to 0px.
- Only transform the button. Never animate margin, top or left.
- Guard the whole effect with @media (hover: hover) and (pointer: fine) so touch devices get a normal button.
- Cap the strength at 0.3 — anything above 0.4 makes the target feel unstable.
- Keep the focus ring intact and do not move the button further than 16px, or keyboard users lose sight of the hit target.

What actually makes it work

Compatibility and performance

EnvironmentBehaviour
Desktop with mouseFull effect. Pointer events are universal, no polyfill needed.
Touch devicesEffect disabled by the hover media query — a normal button remains.
KeyboardUnchanged. pointerleave never fires, so the button stays in place.

The cost here is negligible: one pointer event per frame per button, a style write on a custom property, and a compositor-only transform. The thing to watch is pointermove frequency — it fires at the device's report rate (often 120Hz+). Because the handler does no layout reads, that is fine; if you ever need the bounding rect, cache it on pointerenter instead of calling getBoundingClientRect() inside the move handler.

When not to use it

Don't apply it to every button. One magnetic element per screen is a deliberate emphasis. Four of them and the page feels twitchy.

Don't use it on the only route to conversion if you can't test it. A button that moves away from a shaky cursor can cost clicks. Cap the displacement and always keep the hit area at least as large as the original button.

Don't stack it with a cursor-following custom cursor. Two things chasing the pointer at once reads as a bug, not a flourish.