A button that magnetically follows cursor proximity with smooth spring physics.
Made by Léo"use client";
import { MagneticButton } from "@/components/unlumen-ui/magnetic-button";
import type { VariantProps } from "class-variance-authority";
import type { magneticButtonVariants } from "@/components/unlumen-ui/magnetic-button";
interface MagneticButtonDemoProps {
radius?: number;
strength?: number;
stiffness?: number;
damping?: number;
variant?: VariantProps<typeof magneticButtonVariants>["variant"];
size?: VariantProps<typeof magneticButtonVariants>["size"];
}
export const MagneticButtonDemo = ({
radius = 100,
strength = 0.5,
stiffness = 150,
damping = 15,
variant = "default",
size = "default",
}: MagneticButtonDemoProps) => {
const springOptions = { stiffness, damping, mass: 0.1 };
return (
<div className="flex flex-wrap items-center justify-center gap-6 p-16">
<MagneticButton
variant={variant}
size={size}
radius={radius}
strength={strength}
springOptions={springOptions}
>
Deploy
</MagneticButton>
<MagneticButton
variant={variant}
size={size}
radius={radius}
strength={strength}
springOptions={springOptions}
>
Preview
</MagneticButton>
<MagneticButton
variant={variant}
size={size}
radius={radius}
strength={strength}
springOptions={springOptions}
>
Cancel
</MagneticButton>
</div>
);
};
export default MagneticButtonDemo;Installation
Install the following dependencies:
Install the following registry dependencies:
Copy and paste the following code into your project:
"use client";
import * as React from "react";
import {
motion,
useMotionValue,
useSpring,
type SpringOptions,
type HTMLMotionProps,
} from "motion/react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const magneticButtonVariants = cva(
"relative inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 select-none",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface MagneticButtonProps
extends Omit<HTMLMotionProps<"button">, "style">,
VariantProps<typeof magneticButtonVariants> {
/** activation radius in px — @default 100 */
radius?: number;
springOptions?: SpringOptions;
/** pull strength multiplier 0–1 — @default 0.5 */
strength?: number;
asChild?: boolean;
}
export function MagneticButton({
children,
radius = 100,
springOptions = { stiffness: 150, damping: 15, mass: 0.1 },
strength = 0.5,
variant,
size,
className,
...props
}: MagneticButtonProps) {
const ref = React.useRef<HTMLButtonElement>(null);
const rawX = useMotionValue(0);
const rawY = useMotionValue(0);
const x = useSpring(rawX, springOptions);
const y = useSpring(rawY, springOptions);
const handleMouseMove = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
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 = e.clientX - cx;
const dy = e.clientY - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < radius) {
const pull = (1 - dist / radius) * strength;
rawX.set(dx * pull);
rawY.set(dy * pull);
}
},
[radius, strength, rawX, rawY],
);
const handleMouseLeave = React.useCallback(() => {
rawX.set(0);
rawY.set(0);
}, [rawX, rawY]);
return (
<motion.button
ref={ref}
style={{ x, y }}
className={cn(magneticButtonVariants({ variant, size }), className)}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
{...props}
>
{children}
</motion.button>
);
}
export { magneticButtonVariants };Update the import paths to match your project setup.
Usage
import { MagneticButton } from "@/components/unlumen-ui/magnetic-button";
<MagneticButton>Click me</MagneticButton>;Custom Radius
Control the activation radius around the button (in pixels).
{
/* Small radius - activates only very close */
}
<MagneticButton radius={50}>Tight Magnetic</MagneticButton>;
{
/* Large radius - activates from far away */
}
<MagneticButton radius={200}>Wide Magnetic</MagneticButton>;Spring Physics
Customize the spring behavior to control how the button follows the cursor.
<MagneticButton
springOptions={{
stiffness: 200,
damping: 20,
mass: 0.1,
}}
>
Snappier Response
</MagneticButton>
<MagneticButton
springOptions={{
stiffness: 100,
damping: 30,
mass: 0.2,
}}
>
Smoother Follow
</MagneticButton>Styling
Style the button with any CSS classes or inline styles.
<MagneticButton className="bg-gradient-to-r from-purple-500 to-pink-500 text-white">
Gradient Button
</MagneticButton>
<MagneticButton className="bg-slate-900 hover:bg-slate-800 text-white px-8 py-4 text-lg font-semibold">
Dark Theme
</MagneticButton>Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Button content |
radius | number | 100 | Magnetic activation radius (px) |
springOptions | SpringOptions | { stiffness: 150, damping: 15, mass: 0.1 } | Motion spring configuration |
className | string | — | Extra CSS classes |
...props | Button HTML attributes | — | Standard button attributes (onClick, etc.) |