Magnetic Button
A call-to-action that leans toward the pointer and springs back when it leaves.
Preview and code
index.html
<button class="mag" type="button"><span>Get started</span></button>
styles.css
.mag{position:relative;border:0;padding:16px 28px;border-radius:999px;background:var(--ink);color:var(--stage);font:600 16px/1 system-ui,sans-serif;cursor:pointer;transition:transform .35s cubic-bezier(.2,.8,.2,1)}
.mag span{display:inline-block;transition:transform .35s cubic-bezier(.2,.8,.2,1)}
.mag:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
@media (prefers-reduced-motion:reduce){.mag,.mag span{transition:none}}
script.js
const b=document.querySelector('.mag');const s=b.querySelector('span');
const still=matchMedia('(prefers-reduced-motion: reduce)').matches;
b.addEventListener('pointermove',e=>{if(still)return;const r=b.getBoundingClientRect();const x=e.clientX-r.left-r.width/2,y=e.clientY-r.top-r.height/2;b.style.transform=`translate(${x*.3}px,${y*.4}px)`;s.style.transform=`translate(${x*.15}px,${y*.2}px)`});
b.addEventListener('pointerleave',()=>{b.style.transform='';s.style.transform=''});
magnetic-button.tsx
"use client";
import { useRef, type PointerEvent, type ReactNode } from "react";
const css = `
.mag{position:relative;border:0;padding:16px 28px;border-radius:999px;background:#0b1020;color:#fff;font:600 16px/1 system-ui,sans-serif;cursor:pointer;transition:transform .35s cubic-bezier(.2,.8,.2,1)}
.mag span{display:inline-block;transition:transform .35s cubic-bezier(.2,.8,.2,1)}
.mag:focus-visible{outline:2px solid #1659f5;outline-offset:3px}
@media (prefers-reduced-motion:reduce){.mag,.mag span{transition:none}}`;
export function MagneticButton({ children = "Get started", pull = 0.3, onClick }: { children?: ReactNode; pull?: number; onClick?: () => void }) {
const btn = useRef<HTMLButtonElement>(null);
const label = useRef<HTMLSpanElement>(null);
const move = (e: PointerEvent<HTMLButtonElement>) => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !btn.current || !label.current) return;
const r = btn.current.getBoundingClientRect();
const x = e.clientX - r.left - r.width / 2;
const y = e.clientY - r.top - r.height / 2;
btn.current.style.transform = `translate(${x * pull}px, ${y * pull * 1.3}px)`;
label.current.style.transform = `translate(${x * pull * 0.5}px, ${y * pull * 0.65}px)`;
};
const leave = () => {
if (btn.current) btn.current.style.transform = "";
if (label.current) label.current.style.transform = "";
};
return (
<>
<style>{css}</style>
<button ref={btn} type="button" className="mag" onPointerMove={move} onPointerLeave={leave} onClick={onClick}>
<span ref={label}>{children}</span>
</button>
</>
);
}
export default MagneticButton;
Install
npx shadcn@latest add https://ownlyplans.com/r/bhaskar/magnetic-button.json
Adds the React files to components/ownlyplans/ in a project set up with shadcn and Tailwind CSS. Or copy them from the Code tab.
About
The button follows the cursor by a fraction of the distance, so it feels pulled without running away. Motion is skipped when the visitor prefers reduced motion.