Magnetic Hover Effect
Original · freeA button that magnetically nudges toward the cursor.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/hover-magnetic.json"use client";
import { useCallback, useRef, useState } from "react";
import type { PointerEvent as ReactPointerEvent, RefObject } from "react";
import { useReducedMotion } from "motion/react";
type Offset = { x: number; y: number };
const ZERO: Offset = { x: 0, y: 0 };
function useMagnetic(strength: number, radius: number) {
const ref = useRef<HTMLElement | null>(null);
const reduce = useReducedMotion();
const [offset, setOffset] = useState<Offset>(ZERO);
const [active, setActive] = useState(false);
const onPointerMove = useCallback(
(event: ReactPointerEvent<HTMLElement>) => {
if (reduce) return;
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = event.clientX - cx;
const dy = event.clientY - cy;
const dist = Math.hypot(dx, dy);
const falloff = Math.max(0, 1 - dist / radius);
setOffset({ x: dx * strength * falloff, y: dy * strength * falloff });
},
[reduce, radius, strength],
);
const onPointerEnter = useCallback(() => setActive(true), []);
const reset = useCallback(() => {
setActive(false);
setOffset(ZERO);
}, []);
return { ref, offset, active, onPointerMove, onPointerEnter, reset, reduce };
}
export default function HoverMagnetic() {
const primary = useMagnetic(0.4, 160);
const label = useMagnetic(0.28, 120);
const ghost = useMagnetic(0.35, 150);
const icon = useMagnetic(0.55, 130);
return (
<section className="relative w-full overflow-hidden bg-white px-6 py-24 text-slate-900 sm:px-10 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes hovmag-sheen {
0% { transform: translateX(-120%) skewX(-18deg); }
100% { transform: translateX(220%) skewX(-18deg); }
}
@keyframes hovmag-pulse {
0%, 100% { opacity: 0.35; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
.hovmag-sheen, .hovmag-pulse { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 [background:radial-gradient(60rem_60rem_at_50%_-10%,theme(colors.indigo.500/0.12),transparent)]"
/>
<div className="relative mx-auto max-w-3xl">
<div className="mb-14 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium tracking-wide text-indigo-700 uppercase dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
<span className="hovmag-pulse h-1.5 w-1.5 rounded-full bg-indigo-500" />
Magnetic hover
</span>
<h2 className="mt-5 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Buttons that lean into your cursor
</h2>
<p className="mx-auto mt-3 max-w-xl text-base text-slate-600 dark:text-slate-400">
Move your pointer near any element below. It nudges toward you with a
soft spring and snaps home when you leave. Keyboard focus and reduced
motion are fully respected.
</p>
</div>
<div className="flex flex-col items-center gap-12">
{/* Primary magnetic CTA */}
<button
ref={primary.ref as RefObject<HTMLButtonElement>}
type="button"
onPointerMove={primary.onPointerMove}
onPointerEnter={primary.onPointerEnter}
onPointerLeave={primary.reset}
onBlur={primary.reset}
style={{
transform: `translate3d(${primary.offset.x}px, ${primary.offset.y}px, 0)`,
transition: primary.reduce
? "none"
: primary.active
? "transform 120ms cubic-bezier(0.33,1,0.68,1)"
: "transform 480ms cubic-bezier(0.22,1,0.36,1)",
}}
className="group relative inline-flex items-center gap-3 overflow-hidden rounded-2xl bg-indigo-600 px-8 py-4 text-base font-semibold text-white shadow-lg shadow-indigo-600/25 outline-none transition-colors hover:bg-indigo-500 focus-visible:ring-4 focus-visible:ring-indigo-500/40 dark:bg-indigo-500 dark:shadow-indigo-500/20 dark:hover:bg-indigo-400"
>
<span className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl">
<span className="hovmag-sheen absolute inset-y-0 left-0 w-1/3 bg-white/25 opacity-0 group-hover:opacity-100 group-hover:[animation:hovmag-sheen_900ms_ease-in-out]" />
</span>
<span
className="pointer-events-none inline-flex"
style={{
transform: `translate3d(${primary.offset.x * 0.35}px, ${primary.offset.y * 0.35}px, 0)`,
transition: primary.reduce
? "none"
: "transform 480ms cubic-bezier(0.22,1,0.36,1)",
}}
>
Start free trial
</span>
<svg
viewBox="0 0 24 24"
className="pointer-events-none h-5 w-5 transition-transform group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 12h14" />
<path d="M13 6l6 6-6 6" />
</svg>
</button>
{/* Secondary row */}
<div className="flex flex-wrap items-center justify-center gap-6">
<a
ref={ghost.ref as RefObject<HTMLAnchorElement>}
href="#hovmag-docs"
onPointerMove={ghost.onPointerMove}
onPointerEnter={ghost.onPointerEnter}
onPointerLeave={ghost.reset}
onBlur={ghost.reset}
style={{
transform: `translate3d(${ghost.offset.x}px, ${ghost.offset.y}px, 0)`,
transition: ghost.reduce
? "none"
: ghost.active
? "transform 120ms cubic-bezier(0.33,1,0.68,1)"
: "transform 480ms cubic-bezier(0.22,1,0.36,1)",
}}
className="inline-flex items-center gap-2 rounded-2xl border border-slate-300 bg-white px-6 py-3.5 text-sm font-semibold text-slate-800 outline-none transition-colors hover:border-slate-400 hover:bg-slate-50 focus-visible:ring-4 focus-visible:ring-slate-400/30 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:border-slate-600 dark:hover:bg-slate-800"
>
Read the docs
</a>
{/* Magnetic icon button */}
<button
ref={icon.ref as RefObject<HTMLButtonElement>}
type="button"
aria-label="Add to favourites"
onPointerMove={icon.onPointerMove}
onPointerEnter={icon.onPointerEnter}
onPointerLeave={icon.reset}
onBlur={icon.reset}
style={{
transform: `translate3d(${icon.offset.x}px, ${icon.offset.y}px, 0)`,
transition: icon.reduce
? "none"
: icon.active
? "transform 110ms cubic-bezier(0.33,1,0.68,1)"
: "transform 460ms cubic-bezier(0.22,1,0.36,1)",
}}
className="group inline-flex h-14 w-14 items-center justify-center rounded-full border border-rose-200 bg-rose-50 text-rose-600 outline-none transition-colors hover:bg-rose-100 focus-visible:ring-4 focus-visible:ring-rose-400/40 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-300 dark:hover:bg-rose-500/20"
>
<svg
viewBox="0 0 24 24"
className="h-6 w-6 transition-transform duration-300 group-hover:scale-110"
fill="currentColor"
aria-hidden="true"
>
<path d="M12 21s-6.7-4.35-9.33-8.24C.9 10.02 1.6 6.4 4.6 5.28 6.7 4.5 9 5.3 10.2 7.1c.3.44.6.44.9 0C12.3 5.3 14.6 4.5 16.7 5.28c3 1.12 3.7 4.74 1.93 7.48C18.7 16.65 12 21 12 21z" />
</svg>
</button>
</div>
{/* Magnetic pill with independent label pull */}
<div
ref={label.ref as RefObject<HTMLDivElement>}
onPointerMove={label.onPointerMove}
onPointerEnter={label.onPointerEnter}
onPointerLeave={label.reset}
style={{
transform: `translate3d(${label.offset.x}px, ${label.offset.y}px, 0)`,
transition: label.reduce
? "none"
: label.active
? "transform 130ms cubic-bezier(0.33,1,0.68,1)"
: "transform 500ms cubic-bezier(0.22,1,0.36,1)",
}}
className="flex items-center gap-4 rounded-full border border-emerald-200 bg-emerald-50 px-6 py-3 dark:border-emerald-500/30 dark:bg-emerald-500/10"
>
<span className="relative flex h-2.5 w-2.5">
<span className="hovmag-pulse absolute inline-flex h-full w-full rounded-full bg-emerald-400" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
</span>
<span
className="text-sm font-medium text-emerald-800 dark:text-emerald-200"
style={{
transform: `translate3d(${label.offset.x * 0.4}px, 0, 0)`,
transition: label.reduce
? "none"
: "transform 500ms cubic-bezier(0.22,1,0.36,1)",
}}
>
All systems operational
</span>
</div>
</div>
<p className="mt-14 text-center text-xs text-slate-500 dark:text-slate-500">
Pull strength falls off with distance. Touch and reduced-motion users get
a clean, static button.
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Card Lift Hover Effect
OriginalA card that lifts with a growing shadow on hover.

Card Glow Hover Effect
OriginalA card with a coloured glow that blooms on hover.

Card Border Draw Hover Effect
OriginalA card whose border draws itself in on hover.

Card Gradient Shift Hover Effect
OriginalA card whose gradient background shifts on hover.

Card Tilt Hover Effect
OriginalA card that tilts in 3D toward the cursor on hover.

Card Spotlight Hover Effect
OriginalA card with a cursor-following radial spotlight.

Link Underline Hover Effect
OriginalAnimated link underline effects: grow, slide and centre-out.

Link Slide Hover Effect
OriginalLinks whose label slides up to a duplicate on hover.

Button Fill Hover Effect
OriginalButtons with a colour fill that sweeps across on hover.

Button Shine Hover Effect
OriginalButtons with a diagonal shine sweep on hover.
Icon Fill Hover Effect
OriginalIcon buttons whose icon and background fill on hover.
Icon Bounce Hover Effect
OriginalIcons that bounce or spin playfully on hover.

