Web
Button
Explore the Snyth registry of premium, high-performance UI blocks.
Preview
Installation
1. Install Dependencies
Ensure you have motion installed in your project.
npm install motionyarn add motionpnpm add motionbun add motion2. Add Component
Using CLI
Use the Snyth CLI to automatically add the button component to your project.
npx snyth add buttonbunx snyth add buttonManual Installation
Create the file at components/file-path/button.tsx.
"use client";
import React, { useRef, useState } from "react";
import {
motion,
useMotionValue,
useSpring,
useTransform,
AnimatePresence,
} from "motion/react";
interface MagneticButtonProps {
label?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
variant?: "primary" | "secondary";
}
interface Ripple {
id: number;
x: number;
y: number;
}
const Button: React.FC<MagneticButtonProps> = ({
label = "Hover me",
onClick,
}) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const [ripples, setRipples] = useState<Ripple[]>([]);
const [isHovered, setIsHovered] = useState<boolean>(false);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const springConfig = { damping: 25, stiffness: 200, mass: 0.6 };
const springX = useSpring(mouseX, springConfig);
const springY = useSpring(mouseY, springConfig);
const textX = useTransform(springX, (val) => val * 0.4);
const textY = useTransform(springY, (val) => val * 0.4);
const glowX = useMotionValue(0);
const glowY = useMotionValue(0);
const springGlowX = useSpring(glowX, { damping: 40, stiffness: 300 });
const springGlowY = useSpring(glowY, { damping: 40, stiffness: 300 });
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!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;
mouseX.set(distanceX * 0.3);
mouseY.set(distanceY * 0.3);
glowX.set(e.clientX - rect.left);
glowY.set(e.clientY - rect.top);
};
const handleMouseLeave = () => {
setIsHovered(false);
mouseX.set(0);
mouseY.set(0);
};
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!buttonRef.current) return;
const rect = buttonRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newRipple: Ripple = {
id: Date.now(),
x,
y,
};
setRipples((prev) => [...prev, newRipple]);
if (onClick) onClick(e);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
}, 1000);
};
const glowBg = useTransform(
[springGlowX, springGlowY],
([x, y]) =>
`radial-gradient(circle 140px at ${x}px ${y}px, var(--glow-color), transparent)`
);
const borderGlowBg = useTransform(
[springGlowX, springGlowY],
([x, y]) =>
`radial-gradient(circle 100px at ${x}px ${y}px, var(--border-glow), transparent)`
);
return (
<motion.button
ref={buttonRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{
x: springX,
y: springY,
} as any}
className={`
relative group overflow-hidden px-10 py-5 rounded-2xl
flex items-center justify-center gap-4 select-none
cursor-none md:cursor-pointer min-w-[240px]
bg-white border-black/10 text-neutral-900
[--glow-color:rgba(168,85,247,0.15)]
[--border-glow:rgba(0,0,0,0.1)]
dark:bg-neutral-950 dark:border-white/10 dark:text-white
dark:[--glow-color:rgba(168,85,247,0.2)]
dark:[--border-glow:rgba(255,255,255,0.15)]
transition-colors duration-300
`}
animate={{
scale: isHovered ? 1.05 : 1,
borderRadius: isHovered ? "32px" : "16px",
borderColor: isHovered
? "rgba(168, 85, 247, 0.4)"
: "transparent",
}}
transition={{ type: "spring", damping: 20, stiffness: 200 }}
>
<div className="absolute inset-0 opacity-[0.03] dark:opacity-[0.04] pointer-events-none mix-blend-overlay">
<svg width="100%" height="100%">
<filter id="grain">
<feTurbulence
type="fractalNoise"
baseFrequency="0.8"
numOctaves="4"
stitchTiles="stitch"
/>
</filter>
<rect width="100%" height="100%" filter="url(#grain)" />
</svg>
</div>
<motion.div
className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-500"
style={{ background: glowBg }}
/>
<motion.div
className="absolute inset-[-1px] rounded-inherit pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-300"
style={{ background: borderGlowBg }}
/>
<AnimatePresence>
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
initial={{ scale: 0, opacity: 0.5 }}
animate={{ scale: 5, opacity: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 1, ease: "circOut" }}
className="absolute bg-neutral-400/20 dark:bg-white/10 rounded-full pointer-events-none z-0"
style={{
width: 100,
height: 100,
left: ripple.x - 50,
top: ripple.y - 50,
}}
/>
))}
</AnimatePresence>
<motion.div
className="relative z-10 flex items-center justify-center gap-4 w-full"
style={{ x: textX, y: textY }}
>
<span className="font-semibold tracking-widest text-xs uppercase text-center whitespace-nowrap">
{label}
</span>
</motion.div>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 h-[1px] bg-gradient-to-r from-transparent via-purple-500/40 to-transparent blur-[1px]" />
</motion.button>
);
};
export default Button;Usage
import Button from "@/components/file-path/button";
export default function Example() {
return (
<div className="flex gap-4">
<Button text="Explore Snyth" />
<Button text="Get Started" />
</div>
);
}