Magnetic Button

The button leans toward the cursor and its label drifts a little further, then settles back when the pointer leaves.

  • GSAP
  • Buttons
  • Pointer
  • Added

AI prompt

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

Demo.tsx
'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>
  )
}
demo.module.css
.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;
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.