← Back to Blog

Building a Magnetic Button with Cursor Physics

Learn how to build a button that's pulled towards the user's cursor — with radius falloff, spring physics, and full accessibility support.

Eduardo CalvoEduardo Calvo
··1 min read

In this tutorial, we'll build the Magnetic Button step by step. Move your cursor near the preview — the button will lean towards you as you add each layer.

Base Button

Start with a plain button — no motion, no tracking.

<button className="rounded-md bg-primary px-6 py-2 text-primary-foreground">  Hover me</button>

Track the Cursor

Measure the distance from the button center to the pointer on mouse move.

const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {  const rect = buttonRef.current!.getBoundingClientRect();  const centerX = rect.left + rect.width / 2;  const centerY = rect.top + rect.height / 2;  const dx = e.clientX - centerX;  const dy = e.clientY - centerY;};

Pull Towards the Cursor

Translate the button proportionally, with a strength factor so it only drifts slightly.

// strength = 0.3 means the button drifts 30% of the distancex.set(dx * strength);y.set(dy * strength);// In JSX<motion.div style={{ x, y }}>  <button>Magnetic</button></motion.div>

Radius Falloff

Only react within a radius. The further from center, the weaker the pull.

const distance = Math.sqrt(dx * dx + dy * dy);if (distance < radius) {  const falloff = 1 - distance / radius;  x.set(dx * strength * falloff);  y.set(dy * strength * falloff);} else {  x.set(0);  y.set(0);}

Spring Return

Wrap the x/y values in useSpring so movement — and the release — feels organic.

const x = useSpring(0, { duration: 0.4, bounce: 0.1 });const y = useSpring(0, { duration: 0.4, bounce: 0.1 });// On mouse leave, snap back:const handleMouseLeave = () => {  x.set(0);  y.set(0);};

Accessibility

Disable the effect on touch devices and when the user prefers reduced motion.

const shouldReduceMotion = useReducedMotion();const [isHoverDevice, setIsHoverDevice] = useState(false);useEffect(() => {  const mq = window.matchMedia("(hover: hover) and (pointer: fine)");  setIsHoverDevice(mq.matches);}, []);const disabled = shouldReduceMotion || !isHoverDevice;
Live PreviewStep 1/6

Key Takeaways

After building this component, you've learned:

  1. Distance math with getBoundingClientRect + Math.sqrt drives the effect
  2. A strength factor < 1 keeps the motion subtle — 0.3–0.4 is a sweet spot
  3. Radius falloff makes the interaction local instead of global
  4. useSpring turns raw deltas into organic motion, including the snap-back
  5. Touch + reduced-motion checks are not optional — always gate hover effects
  6. Only animate transform (via x/y motion values) for 60fps performance

Install the Component

npx smoothui-cli add magnetic-button

Check out the full documentation for all props and variations.

Share:

More Posts