Magnetic Button

Button

Magnetic Button

Magnetic Button adds a satisfying cursor-tracking pull effect to any
Framer button component.

HOW IT WORKS
Apply the withMagnet code override to any button on your canvas.
The button will gravitate toward the cursor when hovered and spring
back smoothly when the cursor leaves.


FEATURES
— Works as a code override — apply to any existing button
— Smooth spring physics (no janky transitions)
— Adjustable pull strength and detection radius
— Automatically disabled on touch/mobile devices
— Zero layout shift — surrounding content is unaffected

HOW TO USE

  1. Add the component to your project

  2. Select your button on the canvas

  3. In the right panel → Code Overrides → choose withMagnet


    import { useState, useEffect, useRef, ComponentType } from "react"
    import { motion, useMotionValue, useSpring } from "framer-motion"
    import { addPropertyControls, ControlType } from "framer"
    
    const SPRING_CONFIG = { damping: 40, stiffness: 400 }
    
    interface MagnetProps {
        magnetStrength?: number
        magnetRadius?: number
        width?: number | string
        height?: number | string
        [key: string]: any
    }
    
    export const withMagnet = (Component: ComponentType<any>): ComponentType<MagnetProps> => {
        return ({
            magnetStrength = 0.18,
            magnetRadius = 100,
            width,
            height,
            ...props
        }: MagnetProps) => {
            const [isHovered, setIsHovered] = useState(false)
            const [isTouchDevice, setIsTouchDevice] = useState(false)
            const x = useMotionValue(0)
            const y = useMotionValue(0)
            const ref = useRef<HTMLDivElement>(null)
            const springX = useSpring(x, SPRING_CONFIG)
            const springY = useSpring(y, SPRING_CONFIG)
    
            useEffect(() => {
                setIsTouchDevice(
                    "ontouchstart" in window || navigator.maxTouchPoints > 0
                )
            }, [])
    
            useEffect(() => {
                if (isTouchDevice) return
    
                const calculateDistance = (e: MouseEvent) => {
                    if (!ref.current) return
    
                    const rect = ref.current.getBoundingClientRect()
                    const centerX = rect.left + rect.width / 2
                    const centerY = rect.top + rect.height / 2
                    const distanceX = e.clientX - centerX
                    const distanceY = e.clientY - centerY
                    const distance = Math.sqrt(distanceX ** 2 + distanceY ** 2)
    
                    if (isHovered && distance < magnetRadius) {
                        x.set(distanceX * magnetStrength)
                        y.set(distanceY * magnetStrength)
                    } else {
                        x.set(0)
                        y.set(0)
                    }
                }
    
                document.addEventListener("mousemove", calculateDistance)
                return () => {
                    document.removeEventListener("mousemove", calculateDistance)
                }
            }, [ref, isHovered, isTouchDevice, magnetStrength, magnetRadius])
    
            if (isTouchDevice) {
                return <Component width={width} height={height} {...props} />
            }
    
            return (
                <motion.div
                    ref={ref}
                    onMouseEnter={() => setIsHovered(true)}
                    onMouseLeave={() => {
                        setIsHovered(false)
                        x.set(0)
                        y.set(0)
                    }}
                    style={{
                        x: springX,
                        y: springY,
                        display: "inline-flex",
                        alignItems: "center",
                        justifyContent: "center",
                        width: width ?? "auto",
                        height: height ?? "auto",
                    }}
                >
                    <Component width={width} height={height} {...props} />
                </motion.div>
            )
        }
    }
    
    addPropertyControls(withMagnet, {
        magnetStrength: {
            type: ControlType.Number,
            title: "Strength",
            defaultValue: 0.18,
            min: 0,
            max: 0.5,
            step: 0.05,
            displayStepper: true,
        },
        magnetRadius: {
            type: ControlType.Number,
            title: "Radius",
            defaultValue: 100,
            min: 50,
            max: 300,
            step: 10,
            displayStepper: true,
        },
    })
    import { useState, useEffect, useRef, ComponentType } from "react"
    import { motion, useMotionValue, useSpring } from "framer-motion"
    import { addPropertyControls, ControlType } from "framer"
    
    const SPRING_CONFIG = { damping: 40, stiffness: 400 }
    
    interface MagnetProps {
        magnetStrength?: number
        magnetRadius?: number
        width?: number | string
        height?: number | string
        [key: string]: any
    }
    
    export const withMagnet = (Component: ComponentType<any>): ComponentType<MagnetProps> => {
        return ({
            magnetStrength = 0.18,
            magnetRadius = 100,
            width,
            height,
            ...props
        }: MagnetProps) => {
            const [isHovered, setIsHovered] = useState(false)
            const [isTouchDevice, setIsTouchDevice] = useState(false)
            const x = useMotionValue(0)
            const y = useMotionValue(0)
            const ref = useRef<HTMLDivElement>(null)
            const springX = useSpring(x, SPRING_CONFIG)
            const springY = useSpring(y, SPRING_CONFIG)
    
            useEffect(() => {
                setIsTouchDevice(
                    "ontouchstart" in window || navigator.maxTouchPoints > 0
                )
            }, [])
    
            useEffect(() => {
                if (isTouchDevice) return
    
                const calculateDistance = (e: MouseEvent) => {
                    if (!ref.current) return
    
                    const rect = ref.current.getBoundingClientRect()
                    const centerX = rect.left + rect.width / 2
                    const centerY = rect.top + rect.height / 2
                    const distanceX = e.clientX - centerX
                    const distanceY = e.clientY - centerY
                    const distance = Math.sqrt(distanceX ** 2 + distanceY ** 2)
    
                    if (isHovered && distance < magnetRadius) {
                        x.set(distanceX * magnetStrength)
                        y.set(distanceY * magnetStrength)
                    } else {
                        x.set(0)
                        y.set(0)
                    }
                }
    
                document.addEventListener("mousemove", calculateDistance)
                return () => {
                    document.removeEventListener("mousemove", calculateDistance)
                }
            }, [ref, isHovered, isTouchDevice, magnetStrength, magnetRadius])
    
            if (isTouchDevice) {
                return <Component width={width} height={height} {...props} />
            }
    
            return (
                <motion.div
                    ref={ref}
                    onMouseEnter={() => setIsHovered(true)}
                    onMouseLeave={() => {
                        setIsHovered(false)
                        x.set(0)
                        y.set(0)
                    }}
                    style={{
                        x: springX,
                        y: springY,
                        display: "inline-flex",
                        alignItems: "center",
                        justifyContent: "center",
                        width: width ?? "auto",
                        height: height ?? "auto",
                    }}
                >
                    <Component width={width} height={height} {...props} />
                </motion.div>
            )
        }
    }
    
    addPropertyControls(withMagnet, {
        magnetStrength: {
            type: ControlType.Number,
            title: "Strength",
            defaultValue: 0.18,
            min: 0,
            max: 0.5,
            step: 0.05,
            displayStepper: true,
        },
        magnetRadius: {
            type: ControlType.Number,
            title: "Radius",
            defaultValue: 100,
            min: 50,
            max: 300,
            step: 10,
            displayStepper: true,
        },
    })
    import { useState, useEffect, useRef, ComponentType } from "react"
    import { motion, useMotionValue, useSpring } from "framer-motion"
    import { addPropertyControls, ControlType } from "framer"
    
    const SPRING_CONFIG = { damping: 40, stiffness: 400 }
    
    interface MagnetProps {
        magnetStrength?: number
        magnetRadius?: number
        width?: number | string
        height?: number | string
        [key: string]: any
    }
    
    export const withMagnet = (Component: ComponentType<any>): ComponentType<MagnetProps> => {
        return ({
            magnetStrength = 0.18,
            magnetRadius = 100,
            width,
            height,
            ...props
        }: MagnetProps) => {
            const [isHovered, setIsHovered] = useState(false)
            const [isTouchDevice, setIsTouchDevice] = useState(false)
            const x = useMotionValue(0)
            const y = useMotionValue(0)
            const ref = useRef<HTMLDivElement>(null)
            const springX = useSpring(x, SPRING_CONFIG)
            const springY = useSpring(y, SPRING_CONFIG)
    
            useEffect(() => {
                setIsTouchDevice(
                    "ontouchstart" in window || navigator.maxTouchPoints > 0
                )
            }, [])
    
            useEffect(() => {
                if (isTouchDevice) return
    
                const calculateDistance = (e: MouseEvent) => {
                    if (!ref.current) return
    
                    const rect = ref.current.getBoundingClientRect()
                    const centerX = rect.left + rect.width / 2
                    const centerY = rect.top + rect.height / 2
                    const distanceX = e.clientX - centerX
                    const distanceY = e.clientY - centerY
                    const distance = Math.sqrt(distanceX ** 2 + distanceY ** 2)
    
                    if (isHovered && distance < magnetRadius) {
                        x.set(distanceX * magnetStrength)
                        y.set(distanceY * magnetStrength)
                    } else {
                        x.set(0)
                        y.set(0)
                    }
                }
    
                document.addEventListener("mousemove", calculateDistance)
                return () => {
                    document.removeEventListener("mousemove", calculateDistance)
                }
            }, [ref, isHovered, isTouchDevice, magnetStrength, magnetRadius])
    
            if (isTouchDevice) {
                return <Component width={width} height={height} {...props} />
            }
    
            return (
                <motion.div
                    ref={ref}
                    onMouseEnter={() => setIsHovered(true)}
                    onMouseLeave={() => {
                        setIsHovered(false)
                        x.set(0)
                        y.set(0)
                    }}
                    style={{
                        x: springX,
                        y: springY,
                        display: "inline-flex",
                        alignItems: "center",
                        justifyContent: "center",
                        width: width ?? "auto",
                        height: height ?? "auto",
                    }}
                >
                    <Component width={width} height={height} {...props} />
                </motion.div>
            )
        }
    }
    
    addPropertyControls(withMagnet, {
        magnetStrength: {
            type: ControlType.Number,
            title: "Strength",
            defaultValue: 0.18,
            min: 0,
            max: 0.5,
            step: 0.05,
            displayStepper: true,
        },
        magnetRadius: {
            type: ControlType.Number,
            title: "Radius",
            defaultValue: 100,
            min: 50,
            max: 300,
            step: 10,
            displayStepper: true,
        },
    })
    import { useState, useEffect, useRef, ComponentType } from "react"
    import { motion, useMotionValue, useSpring } from "framer-motion"
    import { addPropertyControls, ControlType } from "framer"
    
    const SPRING_CONFIG = { damping: 40, stiffness: 400 }
    
    interface MagnetProps {
        magnetStrength?: number
        magnetRadius?: number
        width?: number | string
        height?: number | string
        [key: string]: any
    }
    
    export const withMagnet = (Component: ComponentType<any>): ComponentType<MagnetProps> => {
        return ({
            magnetStrength = 0.18,
            magnetRadius = 100,
            width,
            height,
            ...props
        }: MagnetProps) => {
            const [isHovered, setIsHovered] = useState(false)
            const [isTouchDevice, setIsTouchDevice] = useState(false)
            const x = useMotionValue(0)
            const y = useMotionValue(0)
            const ref = useRef<HTMLDivElement>(null)
            const springX = useSpring(x, SPRING_CONFIG)
            const springY = useSpring(y, SPRING_CONFIG)
    
            useEffect(() => {
                setIsTouchDevice(
                    "ontouchstart" in window || navigator.maxTouchPoints > 0
                )
            }, [])
    
            useEffect(() => {
                if (isTouchDevice) return
    
                const calculateDistance = (e: MouseEvent) => {
                    if (!ref.current) return
    
                    const rect = ref.current.getBoundingClientRect()
                    const centerX = rect.left + rect.width / 2
                    const centerY = rect.top + rect.height / 2
                    const distanceX = e.clientX - centerX
                    const distanceY = e.clientY - centerY
                    const distance = Math.sqrt(distanceX ** 2 + distanceY ** 2)
    
                    if (isHovered && distance < magnetRadius) {
                        x.set(distanceX * magnetStrength)
                        y.set(distanceY * magnetStrength)
                    } else {
                        x.set(0)
                        y.set(0)
                    }
                }
    
                document.addEventListener("mousemove", calculateDistance)
                return () => {
                    document.removeEventListener("mousemove", calculateDistance)
                }
            }, [ref, isHovered, isTouchDevice, magnetStrength, magnetRadius])
    
            if (isTouchDevice) {
                return <Component width={width} height={height} {...props} />
            }
    
            return (
                <motion.div
                    ref={ref}
                    onMouseEnter={() => setIsHovered(true)}
                    onMouseLeave={() => {
                        setIsHovered(false)
                        x.set(0)
                        y.set(0)
                    }}
                    style={{
                        x: springX,
                        y: springY,
                        display: "inline-flex",
                        alignItems: "center",
                        justifyContent: "center",
                        width: width ?? "auto",
                        height: height ?? "auto",
                    }}
                >
                    <Component width={width} height={height} {...props} />
                </motion.div>
            )
        }
    }
    
    addPropertyControls(withMagnet, {
        magnetStrength: {
            type: ControlType.Number,
            title: "Strength",
            defaultValue: 0.18,
            min: 0,
            max: 0.5,
            step: 0.05,
            displayStepper: true,
        },
        magnetRadius: {
            type: ControlType.Number,
            title: "Radius",
            defaultValue: 100,
            min: 50,
            max: 300,
            step: 10,
            displayStepper: true,
        },
    })

New components, straight to your inbox

CONTACT

hello@designely.studio

© 2026
Designely Studio · Manchester, UK

New components, straight to your inbox

CONTACT

hello@designely.studio

© 2026
Designely Studio · Manchester, UK

New components, straight to your inbox

CONTACT

hello@designely.studio

© 2026
Designely Studio · Manchester, UK

New components, straight to your inbox

CONTACT

hello@designely.studio

© 2026
Designely Studio · Manchester, UK