Magnetic
Magnetic hover effect component for React. Elements smoothly follow cursor with spring physics. Perfect for buttons, cards, and interactive UI.
Last updated on
import { Magnetic } from "@/components/ui/magnetic";
export function MagneticDemo() {
return (
<div className="flex min-h-[300px] items-center justify-center">
<Magnetic>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg transition-colors hover:bg-primary/90">
Hover Me
</button>
</Magnetic>
</div>
);
}Installation
CLI
npx shadcn@latest add "https://jolyui.dev/r/magnetic"Manual
Install the following dependencies:
npm install motionCopy and paste the following code into your project.
"use client";
import {
motion,
type SpringOptions,
useMotionValue,
useSpring,
} from "motion/react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
const SPRING_CONFIG = { stiffness: 26.7, damping: 4.1, mass: 0.2 };
export type MagneticProps = {
children: React.ReactNode;
intensity?: number;
range?: number;
actionArea?: "self" | "parent" | "global";
springOptions?: SpringOptions;
};
export function Magnetic({
children,
intensity = 0.6,
range = 100,
actionArea = "self",
springOptions = SPRING_CONFIG,
}: MagneticProps) {
const [isHovered, setIsHovered] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const springX = useSpring(x, springOptions);
const springY = useSpring(y, springOptions);
useEffect(() => {
const calculateDistance = (e: MouseEvent) => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distanceX = e.clientX - centerX;
const distanceY = e.clientY - centerY;
const absoluteDistance = Math.sqrt(distanceX ** 2 + distanceY ** 2);
if (isHovered && absoluteDistance <= range) {
const scale = 1 - absoluteDistance / range;
x.set(distanceX * intensity * scale);
y.set(distanceY * intensity * scale);
} else {
x.set(0);
y.set(0);
}
}
};
document.addEventListener("mousemove", calculateDistance);
return () => {
document.removeEventListener("mousemove", calculateDistance);
};
}, [isHovered, intensity, range, x.set, y.set]);
useEffect(() => {
if (actionArea === "parent" && ref.current?.parentElement) {
const parent = ref.current.parentElement;
const handleParentEnter = () => setIsHovered(true);
const handleParentLeave = () => setIsHovered(false);
parent.addEventListener("mouseenter", handleParentEnter);
parent.addEventListener("mouseleave", handleParentLeave);
return () => {
parent.removeEventListener("mouseenter", handleParentEnter);
parent.removeEventListener("mouseleave", handleParentLeave);
};
} else if (actionArea === "global") {
setIsHovered(true);
}
}, [actionArea]);
const handleMouseEnter = () => {
if (actionArea === "self") {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
if (actionArea === "self") {
setIsHovered(false);
x.set(0);
y.set(0);
}
};
return (
<motion.div
ref={ref}
onMouseEnter={actionArea === "self" ? handleMouseEnter : undefined}
onMouseLeave={actionArea === "self" ? handleMouseLeave : undefined}
style={{
x: springX,
y: springY,
}}
>
{children}
</motion.div>
);
}Layout
import { Magnetic } from "@/components/ui/magnetic";
<Magnetic>
<button>Hover Me</button>
</Magnetic>Examples
Intensity
Control the strength of the magnetic effect with the intensity prop. Higher values create stronger attraction.
import { Magnetic } from "@/components/ui/magnetic";
export function MagneticIntensityDemo() {
return (
<div className="flex min-h-[300px] flex-wrap items-center justify-center gap-8">
<Magnetic intensity={0.3}>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Low (0.3)
</button>
</Magnetic>
<Magnetic intensity={0.6}>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Medium (0.6)
</button>
</Magnetic>
<Magnetic intensity={1.2}>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
High (1.2)
</button>
</Magnetic>
</div>
);
}Range
Adjust the activation distance with the range prop. This defines how far from the element the effect begins.
import { Magnetic } from "@/components/ui/magnetic";
export function MagneticRangeDemo() {
return (
<div className="flex min-h-[300px] flex-wrap items-center justify-center gap-8">
<Magnetic range={50}>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Small Range (50px)
</button>
</Magnetic>
<Magnetic range={100}>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Medium Range (100px)
</button>
</Magnetic>
<Magnetic range={200}>
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Large Range (200px)
</button>
</Magnetic>
</div>
);
}Action Area
Define where the magnetic effect should be triggered using the actionArea prop.
import { Magnetic } from "@/components/ui/magnetic";
export function MagneticActionAreaDemo() {
return (
<div className="flex min-h-[400px] flex-col items-center justify-center gap-12">
<div className="flex flex-col items-center gap-2">
<p className="text-muted-foreground text-sm">Self (hover the button)</p>
<Magnetic actionArea="self">
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Self Action Area
</button>
</Magnetic>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-muted-foreground text-sm">
Parent (hover the container)
</p>
<div className="rounded-lg border-2 border-muted-foreground/30 border-dashed p-8">
<Magnetic actionArea="parent">
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Parent Action Area
</button>
</Magnetic>
</div>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-muted-foreground text-sm">Global (always active)</p>
<Magnetic actionArea="global">
<button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
Global Action Area
</button>
</Magnetic>
</div>
</div>
);
}Cards Example
A practical example showing the magnetic effect applied to card components.
import { Magnetic } from "@/components/ui/magnetic";
export function MagneticCardsDemo() {
const cards = [
{ title: "Design", description: "Beautiful UI components", icon: "🎨" },
{
title: "Development",
description: "Clean and efficient code",
icon: "💻",
},
{ title: "Animation", description: "Smooth interactions", icon: "✨" },
];
return (
<div className="flex min-h-[400px] flex-wrap items-center justify-center gap-6 p-8">
{cards.map((card, index) => (
<Magnetic key={index} intensity={0.4} range={120}>
<div className="flex h-48 w-56 flex-col items-center justify-center gap-3 rounded-xl border bg-card p-6 shadow-lg transition-shadow hover:shadow-xl">
<span className="text-4xl">{card.icon}</span>
<h3 className="font-semibold text-lg">{card.title}</h3>
<p className="text-center text-muted-foreground text-sm">
{card.description}
</p>
</div>
</Magnetic>
))}
</div>
);
}API Reference
Magnetic
The main magnetic wrapper component that applies smooth cursor-following animations to its children.
Prop
Type
How is this guide?
Image Sphere
3D rotating image gallery sphere for React. Drag to rotate, click to expand. Built with CSS 3D transforms and spring physics. Perfect for portfolios.
Video Player
Custom React video player with playback controls, volume slider, fullscreen mode, keyboard shortcuts, and progress bar. Fully accessible and styleable.