Magnetic Button - Interactive Hover Effect Component

A button component that creates a magnetic pull effect, subtly following the cursor when hovering nearby. Features elastic snap-back animation, multiple variants, customizable strength, and spring physics for smooth interactions.

Installation

npx shadcn@latest add "https://ui-struct.vercel.app/r/magnetic-button"

Usage

import MagneticButton from "@/components/ui/magnetic-button";
<MagneticButton>Click Me</MagneticButton>

Preview

Move your cursor near the button to see the magnetic pull effect.

Variants

Five built-in style variants to match your design system.

Variant Options

VariantDescription
defaultSolid background, inverts in dark mode
outlineBorder only, fills on hover
ghostNo background, subtle hover effect
glowBlue with glowing shadow effect
gradientPurple to orange gradient

Sizes

Three size options for different use cases.

Magnetic Strength

Control how strongly the button is pulled toward the cursor.

Strength: 0.2
Strength: 0.5
Strength: 0.8

Strength Props

PropTypeDefaultDescription
magneticStrengthnumber0.4How strongly the button follows cursor (0-1)
magneticRadiusnumber150Pixel radius where magnetic effect activates

With Icons

Combine with icons for enhanced visual appeal.

Spring Physics

Customize the elastic snap-back behavior with spring configuration.

High stiffness, low damping
Low stiffness, low damping
Balanced

Spring Config

PropertyDescription
stiffnessHigher = faster snap back (default: 300)
dampingHigher = less bounce (default: 20)

Props

PropTypeDefaultDescription
childrenReactNode-Button content
variant'default' | 'outline' | 'ghost' | 'glow' | 'gradient''default'Visual style
size'sm' | 'md' | 'lg''md'Button size
magneticStrengthnumber0.4Pull strength (0-1)
magneticRadiusnumber150Activation radius in pixels
springConfig{ stiffness: number; damping: number }{ stiffness: 300, damping: 20 }Spring physics
disabledbooleanfalseDisable interactions
onClick() => void-Click handler
classNamestring-Additional CSS classes

Full Code

'use client';
import { useRef, useState } from 'react';
import { motion, useSpring } from 'framer-motion';
interface MagneticButtonProps {
children: React.ReactNode;
className?: string;
magneticStrength?: number;
magneticRadius?: number;
springConfig?: { stiffness: number; damping: number };
variant?: 'default' | 'outline' | 'ghost' | 'glow' | 'gradient';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}
const sizeClasses = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3 text-base',
lg: 'px-8 py-4 text-lg',
};
const variantClasses = {
default:
'bg-neutral-900 text-white hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-100',
outline:
'border-2 border-neutral-900 text-neutral-900 hover:bg-neutral-900 hover:text-white dark:border-white dark:text-white dark:hover:bg-white dark:hover:text-neutral-900',
ghost:
'text-neutral-900 hover:bg-neutral-100 dark:text-white dark:hover:bg-neutral-800',
glow: 'bg-blue-600 text-white shadow-[0_0_20px_rgba(59,130,246,0.5)] hover:shadow-[0_0_30px_rgba(59,130,246,0.7)] hover:bg-blue-500',
gradient:
'bg-gradient-to-r from-purple-600 via-pink-600 to-orange-500 text-white hover:opacity-90',
};
export default function MagneticButton({
children,
className = '',
magneticStrength = 0.4,
magneticRadius = 150,
springConfig = { stiffness: 300, damping: 20 },
variant = 'default',
size = 'md',
disabled = false,
onClick,
}: MagneticButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null);
const [isHovered, setIsHovered] = useState(false);
const x = useSpring(0, springConfig);
const y = useSpring(0, springConfig);
const scale = useSpring(1, { stiffness: 400, damping: 25 });
const handleMouseMove = (e: React.MouseEvent) => {
if (disabled || !buttonRef.current) return;
const rect = buttonRef.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 distance = Math.hypot(distanceX, distanceY);
if (distance < magneticRadius) {
const strength = (1 - distance / magneticRadius) * magneticStrength;
x.set(distanceX * strength);
y.set(distanceY * strength);
}
};
const handleMouseEnter = () => {
if (!disabled) {
setIsHovered(true);
scale.set(1.05);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
x.set(0);
y.set(0);
scale.set(1);
};
return (
<motion.button
ref={buttonRef}
style={{ x, y, scale }}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={onClick}
disabled={disabled}
className={`
relative inline-flex items-center justify-center
font-medium rounded-full
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500
disabled:opacity-50 disabled:cursor-not-allowed
${sizeClasses[size]}
${variantClasses[variant]}
${className}
`}
>
{children}
</motion.button>
);
}