Cursor-reactive button that attracts its surface and inner label toward the pointer with configurable attraction strength, return spring, hover scale, and touch-device fallback.
Preview
Customize
6 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add magnetic-buttonWrites components/ui/magnetic-button.tsx and components/styles/magnetic-button.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { MagneticButton } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <MagneticButton />6 );7}Props
Accessibility
Uses a semantic <button>, ignores magnetic displacement on coarse/touch pointers (pointerType === 'touch') and under prefers-reduced-motion, and maintains a visible :focus-visible outline.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import { motion, useMotionValue, useSpring, useReducedMotion } from "motion/react";5import styles from "../styles/magnetic-button.module.css";6 7/** Native button attributes whose names collide with Motion's animation and gesture handlers. */8type MotionConflictingKeys = "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd";9 10export interface MagneticButtonProps11 extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, MotionConflictingKeys> {12 /** Button label */13 label?: string;14 /** Attraction strength multiplier */15 strength?: number;16 /** Return spring stiffness */17 stiffness?: number;18 /** Return spring damping */19 damping?: number;20 /** Hover scale multiplier */21 hoverScale?: number;22 /** Visual variant */23 variant?: "solid" | "outline" | "ghost";24}25 26export function MagneticButton({27 children,28 label = "Explore 24 Components",29 strength = 0.36,30 stiffness = 230,31 damping = 18,32 hoverScale = 1.04,33 variant = "solid",34 className = "",35 ...rest36}: MagneticButtonProps) {37 const ref = React.useRef<HTMLButtonElement>(null);38 const prefersReducedMotion = useReducedMotion();39 40 const rawX = useMotionValue(0);41 const rawY = useMotionValue(0);42 const x = useSpring(rawX, { stiffness, damping, mass: 0.4 });43 const y = useSpring(rawY, { stiffness, damping, mass: 0.4 });44 45 const handlePointerMove = (e: React.PointerEvent<HTMLButtonElement>) => {46 if (prefersReducedMotion || e.pointerType === "touch" || !ref.current) return;47 const rect = ref.current.getBoundingClientRect();48 const dx = (e.clientX - (rect.left + rect.width / 2)) * strength;49 const dy = (e.clientY - (rect.top + rect.height / 2)) * strength;50 rawX.set(dx);51 rawY.set(dy);52 };53 54 const handlePointerLeave = () => {55 rawX.set(0);56 rawY.set(0);57 };58 59 return (60 <motion.button61 ref={ref}62 type="button"63 onPointerMove={handlePointerMove}64 onPointerLeave={handlePointerLeave}65 whileHover={prefersReducedMotion ? undefined : { scale: hoverScale }}66 whileTap={prefersReducedMotion ? undefined : { scale: 0.97 }}67 style={{68 x: prefersReducedMotion ? 0 : x,69 y: prefersReducedMotion ? 0 : y,70 }}71 className={`${styles.button} ${styles[variant]} ${className}`.trim()}72 {...rest}73 >74 <span className={styles.label}>{children ?? label}</span>75 </motion.button>76 );77}