interaction·free
Magnetic Button
Button that is pulled toward the cursor with an elastic snap-back.
Installation
terminal
pnpm add gsap @gsap/reactThen copy the component below into src/components/motion/magnetic-button.tsx. It expects @/lib/gsap (plugin registration) and @/lib/utils (cn).
Source
src/components/motion/magnetic-button.tsx
"use client";
import { useRef, type ButtonHTMLAttributes, type ReactNode } from "react";
import { gsap, useGSAP } from "@/lib/gsap";
import { cn } from "@/lib/utils";
export interface MagneticButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
/** How far the button follows the cursor (0–1). */
strength?: number;
/** Radius in px within which the pull is active. */
radius?: number;
}
export function MagneticButton({
children,
className,
strength = 0.35,
radius = 120,
...props
}: MagneticButtonProps) {
const ref = useRef<HTMLButtonElement>(null);
const inner = useRef<HTMLSpanElement>(null);
useGSAP(
() => {
const el = ref.current;
const label = inner.current;
if (!el || !label) return;
const xTo = gsap.quickTo(el, "x", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
const yTo = gsap.quickTo(el, "y", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
const lxTo = gsap.quickTo(label, "x", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
const lyTo = gsap.quickTo(label, "y", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
const onMove = (e: MouseEvent) => {
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = e.clientX - cx;
const dy = e.clientY - cy;
const dist = Math.hypot(dx, dy);
if (dist < radius) {
xTo(dx * strength);
yTo(dy * strength);
lxTo(dx * strength * 0.4);
lyTo(dy * strength * 0.4);
} else {
xTo(0);
yTo(0);
lxTo(0);
lyTo(0);
}
};
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
},
{ scope: ref, dependencies: [strength, radius] }
);
return (
<button
ref={ref}
className={cn(
"relative inline-flex items-center justify-center rounded-full bg-foreground px-7 py-3 text-sm font-medium text-background will-change-transform",
className
)}
{...props}
>
<span ref={inner} className="inline-block will-change-transform">
{children}
</span>
</button>
);
}