Magnetic Button
The button leans toward the cursor and its label drifts a little further, then settles back when the pointer leaves.
Build a React button with a magnetic hover effect using GSAP. Wrap the button in a larger invisible hit zone with about 40px of padding. On pointermove inside the zone, measure the cursor offset from the button centre and tween the button toward it by 35% of that offset, and the inner label by a further 15%, with gsap.to (duration 0.4, ease "power3.out"). On pointerleave, tween both back to x: 0, y: 0 over 0.6s with "expo.out". Animate transform only. Skip the effect entirely when prefers-reduced-motion is set. The button label reads "Get started".
Source
'use client'
import gsap from 'gsap'
import s from './demo.module.css'
export default function MagneticButton({ labels }: { labels: Record<string, string> }) {
const reduced = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (reduced()) return
const button = e.currentTarget.firstElementChild as HTMLElement
const r = button.getBoundingClientRect()
const x = e.clientX - r.left - r.width / 2
const y = e.clientY - r.top - r.height / 2
gsap.to(button, { x: x * 0.35, y: y * 0.35, duration: 0.4, ease: 'power3.out' })
gsap.to(button.firstElementChild, { x: x * 0.15, y: y * 0.15, duration: 0.4, ease: 'power3.out' })
}
const onLeave = (e: React.PointerEvent<HTMLDivElement>) => {
const button = e.currentTarget.firstElementChild as HTMLElement
gsap.to([button, button.firstElementChild], { x: 0, y: 0, duration: 0.6, ease: 'expo.out' })
}
return (
<div className={s.zone} onPointerMove={onMove} onPointerLeave={onLeave}>
<button type="button" className={s.button}>
<span className={s.label}>{labels.cta}</span>
</button>
</div>
)
}
.zone {
padding: 40px;
}
.button {
display: inline-flex;
align-items: center;
height: 52px;
padding: 0 28px;
border: 0;
border-radius: 999px;
background: var(--stage-ink);
color: var(--stage-bg);
font: 500 var(--text-base) / 1 var(--font-body);
white-space: nowrap;
cursor: pointer;
will-change: transform;
}
.button:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 3px;
}
.label {
display: inline-block;
will-change: transform;
}