*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
}
.demo {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
}
.hint {
color: #475569;
font-size: 0.875rem;
margin-bottom: 1rem;
}
/* --- Magnetic button --- */
.magnet-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 1rem 2.5rem;
border-radius: 999px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: none;
outline: none;
/* Smooth snap-back */
transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1);
will-change: transform;
}
/* Inner text — moves slightly more for depth */
.magnet-btn__inner {
display: block;
transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1);
pointer-events: none;
}
.magnet-btn--primary {
background: #0ea5e9;
color: #ffffff;
box-shadow:
0 0 0 0 rgba(14, 165, 233, 0),
0 4px 24px rgba(14, 165, 233, 0.3);
transition:
transform 0.4s cubic-bezier(0.23, 1, 0.32, 1),
box-shadow 0.3s ease;
}
.magnet-btn--primary:hover {
box-shadow:
0 0 0 4px rgba(14, 165, 233, 0.15),
0 8px 32px rgba(14, 165, 233, 0.45);
}
.magnet-btn--ghost {
background: transparent;
color: #cbd5e1;
border: 1.5px solid #334155;
transition:
transform 0.4s cubic-bezier(0.23, 1, 0.32, 1),
border-color 0.2s ease,
color 0.2s ease;
}
.magnet-btn--ghost:hover {
border-color: #94a3b8;
color: #f1f5f9;
}
/* Disable for reduced motion */
@media (prefers-reduced-motion: reduce) {
.magnet-btn,
.magnet-btn__inner {
transition: none;
transform: none !important;
}
}
// Magnetic Button — cursor proximity pull effect
(function () {
"use strict";
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const STRENGTH = 0.35; // pull intensity (0–1)
const RADIUS = 100; // activation radius in px
const buttons = document.querySelectorAll("[data-magnetic]");
buttons.forEach((btn) => {
const inner = btn.querySelector(".magnet-btn__inner");
document.addEventListener("mousemove", (e) => {
const rect = btn.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = e.clientX - centerX;
const dy = e.clientY - centerY;
const distance = Math.hypot(dx, dy);
if (distance < RADIUS) {
const pull = (1 - distance / RADIUS) * STRENGTH;
const moveX = dx * pull;
const moveY = dy * pull;
btn.style.transform = `translate(${moveX}px, ${moveY}px)`;
if (inner) {
inner.style.transform = `translate(${moveX * 0.4}px, ${moveY * 0.4}px)`;
}
} else {
btn.style.transform = "";
if (inner) inner.style.transform = "";
}
});
btn.addEventListener("mouseleave", () => {
btn.style.transform = "";
if (inner) inner.style.transform = "";
});
});
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Magnetic Button</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<p class="hint">Move your cursor near the buttons</p>
<button class="magnet-btn magnet-btn--primary" data-magnetic>
<span class="magnet-btn__inner">Browse Library</span>
</button>
<button class="magnet-btn magnet-btn--ghost" data-magnetic>
<span class="magnet-btn__inner">View Docs</span>
</button>
</div>
<script src="script.js"></script>
</body>
</html>
import { useRef, useCallback } from "react";
interface MagneticButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
strength?: number;
radius?: number;
variant?: "primary" | "ghost";
}
export function MagneticButton({
children,
strength = 0.35,
radius = 100,
variant = "primary",
className = "",
...props
}: MagneticButtonProps) {
const btnRef = useRef<HTMLButtonElement>(null);
const innerRef = useRef<HTMLSpanElement>(null);
const reducedMotion =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
if (reducedMotion) return;
const btn = btnRef.current;
if (!btn) return;
const rect = btn.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = e.clientX - centerX;
const dy = e.clientY - centerY;
const distance = Math.hypot(dx, dy);
if (distance < radius) {
const pull = (1 - distance / radius) * strength;
btn.style.transform = `translate(${dx * pull}px, ${dy * pull}px)`;
if (innerRef.current) {
innerRef.current.style.transform = `translate(${dx * pull * 0.4}px, ${dy * pull * 0.4}px)`;
}
}
},
[strength, radius, reducedMotion]
);
const handleMouseLeave = useCallback(() => {
if (btnRef.current) btnRef.current.style.transform = "";
if (innerRef.current) innerRef.current.style.transform = "";
}, []);
const baseStyles =
"relative inline-flex items-center justify-center px-10 py-4 rounded-full text-base font-semibold cursor-pointer outline-none border-none";
const variantStyles = {
primary:
"bg-sky-500 text-white shadow-[0_4px_24px_rgba(14,165,233,0.3)] hover:shadow-[0_8px_32px_rgba(14,165,233,0.45)]",
ghost:
"bg-transparent text-slate-300 border border-slate-700 hover:border-slate-500 hover:text-slate-100",
};
return (
<button
ref={btnRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={`${baseStyles} ${variantStyles[variant]} ${className}`}
style={{
transition: "transform 0.4s cubic-bezier(0.23,1,0.32,1), box-shadow 0.3s ease",
willChange: "transform",
}}
{...props}
>
<span
ref={innerRef}
style={{ transition: "transform 0.4s cubic-bezier(0.23,1,0.32,1)", pointerEvents: "none" }}
>
{children}
</span>
</button>
);
}
// Demo
export default function MagneticButtonDemo() {
return (
<div className="min-h-screen bg-slate-900 flex flex-col items-center justify-center gap-8">
<p className="text-slate-500 text-sm">Move your cursor near the buttons</p>
<MagneticButton variant="primary">Browse Library</MagneticButton>
<MagneticButton variant="ghost">View Docs</MagneticButton>
</div>
);
}