CoreInteractive Buttons and Controlsmotion/react

MagneticButton

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 props
0.36
230
18
1.04

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add magnetic-button

Writes 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.

example.tsx
1import { MagneticButton } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <MagneticButton />6  );7}

Props

PropTypeDefault
label

Button label text.

string'Explore 24 Components'
strength

Cursor attraction multiplier (0.1–0.75).

number0.36
stiffness

Return spring stiffness.

number230
damping

Return spring damping.

number18
hoverScale

Scale factor on pointer hover.

number1.04
variant

Visual surface style.

'solid' | 'outline' | 'ghost''solid'

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

components/ui/magnetic-button.tsx
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}