FRAMER FORGE
← All posts
interactioncursorbuttonsoverrides

Build a cursor-magnetic button in Framer

The micro-interaction that makes a site feel alive. Step-by-step pattern that doesn't jitter, with the math behind why fractional magnetism feels natural.

April 21, 2026·By Kaiborg

A cursor-magnetic button is one of the most satisfying micro-interactions on the web. The button subtly leans toward the cursor when it's nearby, snaps back when you leave, and reads as "this site is alive."

It's also one of the harder effects to nail. Most tutorials give you a janky version where the button overshoots, jitters, or never quite returns to center. This walk-through covers the pattern that actually works.

The effect

When the cursor is inside the button's bounding box, the button translates toward the cursor by a configurable fraction of the cursor's offset from center. When the cursor leaves, the button eases back to position (0, 0).

The key insight: the magnetism strength is a fraction, not an absolute. If the cursor is 40px from center and the strength is 0.4, the button moves 16px toward it. That fractional relationship is what makes the effect feel natural instead of follow-the-pointer-exactly.

The core math

import type { ComponentType } from "react"
import { useState, useRef } from "react"
import { motion } from "framer-motion"

export function withMagnetic(Component): ComponentType {
  return (props) => {
    const ref = useRef<HTMLDivElement>(null)
    const [pos, setPos] = useState({ x: 0, y: 0 })

    const handleMouseMove = (e: React.MouseEvent) => {
      if (!ref.current) return
      const rect = ref.current.getBoundingClientRect()
      const cx = rect.left + rect.width / 2
      const cy = rect.top + rect.height / 2
      // strength = 0.4 — adjust between 0 and 1 for more/less pull
      setPos({ x: (e.clientX - cx) * 0.4, y: (e.clientY - cy) * 0.4 })
    }

    const handleMouseLeave = () => setPos({ x: 0, y: 0 })

    return (
      <motion.div
        ref={ref}
        animate={{ x: pos.x, y: pos.y }}
        transition={{ type: "spring", stiffness: 150, damping: 15 }}
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
      >
        <Component {...props} />
      </motion.div>
    )
  }
}

Three things to call out:

  • getBoundingClientRect gives us the button's center in viewport coordinates. e.clientX / Y is already viewport-relative, so the subtraction is clean.
  • type: "spring" in the framer-motion transition is what makes the return feel natural. Linear or ease-out transitions both feel mechanical. Spring with stiffness ~150 and damping ~15 is a sweet spot — adjust either to taste.
  • onMouseLeave resets position. Without this, the button stays wherever the cursor was when it left the bounding box.

What's missing from the snippet above

The version above works but has rough edges:

  • No bounding margin — currently the magnetism only kicks in inside the button. The slick version triggers in a buffer zone around the button too (e.g. 30px outside).
  • No prop control over strength — you'd want magnetism and returnEase as override props you can adjust from Framer's right sidebar.
  • No accessibility consideration — keyboard focus should bypass the magnetism (it confuses screen readers).
  • No reduced-motion fallbackprefers-reduced-motion: reduce users should get a static button.

Production-ready Framer overrides need all four. We've packaged the polished version on our marketplace — drop it on a button frame, tweak the props, ship:

OVRMagnetic Button$8

Need a variation?

The Forge generates Framer overrides from natural-language prompts. "Magnetic button but only triggers within 50px of the button" or "magnetic button that also scales 1.05x on hover" — describe it, get the code. Useful when you've hit the limits of what the marketplace version's props expose.

Need a custom version? The Forge writes it.

Try the Forge