Magnetic Button

A button that magnetically follows cursor proximity with smooth spring physics.

Made by Léo
Open in
demo-magnetic-button.tsx
"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:

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

PropTypeDefaultDescription
childrenReact.ReactNodeButton content
radiusnumber100Magnetic activation radius (px)
springOptionsSpringOptions{ stiffness: 150, damping: 15, mass: 0.1 }Motion spring configuration
classNamestringExtra CSS classes
...propsButton HTML attributesStandard button attributes (onClick, etc.)

Built by Léo from Unlumen :3

Last updated: 3/15/2026