Back to Dictionary
Scroll & Navigation
●●○Customize
Context dependent

Magnetic Snap Button

When the mouse gets close to this button, the entire button (and its text) physically pulls towards the cursor, as if magnetized. It snaps back playfully when the mouse leaves.

Use

Minimalist portfolio navigation

Avoid

Dense forms

Safer Alternative

No safer default listed.

Risk Level

Low risk

Live Demo

Magnetic Snapping

What It Is

When the mouse gets close to this button, the entire button (and its text) physically pulls towards the cursor, as if magnetized. It snaps back playfully when the mouse leaves.

When to Use

  • Minimalist portfolio navigation
  • Hamburger menu icons
  • Floating Action Buttons (FABs)

When NOT to Use

  • Dense forms
  • Standard native-feeling applications

Configuration Tips

  • 01Map mouse coordinates to X/Y transforms within a specific radius, divided by a friction coefficient (e.g. / 2)

You've Seen It In

Awwwards SitesCreative Portfolios

AI Implementation Prompts

Move from a fast scaffold to production details, then tune timing and edge states.

Write a React component for a magnetic button. As the mouse moves over it, the x and y transform of the button should follow the cursor slightly.

Reference Implementation

The same implementation approach used by the live preview, kept compact enough to inspect and adapt.

"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { useRef, MouseEvent, useState, useEffect } from "react";

export function MagneticButtonDemo({ isPlaying = false }: { isPlaying?: boolean }) {
    const ref = useRef<HTMLButtonElement>(null);
    const [isInteracting, setIsInteracting] = useState(false);

    "text-stone-400 italic">// Mouse relative to the center of the button
    const x = useMotionValue(0);
    const y = useMotionValue(0);

    "text-stone-400 italic">// Spring physics for smooth snapping back
    const springConfig = { damping: 15, stiffness: 150, mass: 0.1 };
    const springX = useSpring(x, springConfig);
    const springY = useSpring(y, springConfig);

    useEffect(() => {
        if (!isPlaying || isInteracting) return;

        let time = 0;
        const loop = setInterval(() => {
            time += 0.05;
            "text-stone-400 italic">// Move in a small figure-8 pattern
            const autoX = Math.sin(time) * 30;
            const autoY = Math.sin(time * 2) * 15;
            x.set(autoX);
            y.set(autoY);
        }, 16);

        return () => {
            clearInterval(loop);
            if (!isInteracting) {
                x.set(0);
                y.set(0);
            }
        };
    }, [isPlaying, isInteracting, x, y]);

    const handleMouseMove = (e: MouseEvent<HTMLButtonElement>) => {
        setIsInteracting(true);
        if (!ref.current) return;
        const { clientX, clientY } = e;
        const { height, width, left, top } = ref.current.getBoundingClientRect();

        "text-stone-400 italic">// Calculate center
        const middleX = clientX - (left + width / 2);
        const middleY = clientY - (top + height / 2);

        "text-stone-400 italic">// Weak pull (divide by friction factor)
        x.set(middleX * 0.3);
        y.set(middleY * 0.3);
    };

    const handleMouseLeave = () => {
        setIsInteracting(false);
        x.set(0);
        y.set(0);
    };

    return (
        <div className="flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-white p-4 shadow-sm ring-1 ring-stone-200 relative group">
            {"text-stone-400 italic">/* Invisible expanded hit area around the button to cast a wider net for the magnetic pull */}
            <div className="p-12">
                <motion.button
                    ref={ref}
                    onMouseMove={handleMouseMove}
                    onMouseLeave={handleMouseLeave}
                    style={{ x: springX, y: springY }}
                    className={`relative flex h-16 w-40 items-center justify-center rounded-full bg-stone-900 border border-stone-800 text-white font-bold tracking-wider shadow-xl transition-shadow outline-none focus-visible:ring-4 focus-visible:ring-stone-200 ${isPlaying ? 'shadow-2xl' : 'group-hover:shadow-2xl'}`}
                >
                    {"text-stone-400 italic">/* The text inside also moves, but slightly faster for a 3D parallax effect */}
                    <motion.span
                        style={{ x: useTransform(springX, v => v * 0.5), y: useTransform(springY, v => v * 0.5) }}
                        className="pointer-events-none"
                    >
                        Hover Me
                    </motion.span>
                </motion.button>
            </div>

            <p className="absolute bottom-6 text-[10px] text-stone-400 font-bold uppercase tracking-[0.2em]">Magnetic Snapping</p>
        </div>
    );
}