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.
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;Key Takeaways
After building this component, you've learned:
- Distance math with
getBoundingClientRect+Math.sqrtdrives the effect - A strength factor < 1 keeps the motion subtle — 0.3–0.4 is a sweet spot
- Radius falloff makes the interaction local instead of global
useSpringturns raw deltas into organic motion, including the snap-back- Touch + reduced-motion checks are not optional — always gate hover effects
- Only animate
transform(viax/ymotion values) for 60fps performance
Install the Component
npx smoothui-cli add magnetic-buttonCheck out the full documentation for all props and variations.