Skip to main content
Back to discovery

Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.

Magnetic Button

A button that translates toward the cursor with spring physics, with the label counter-drifting to create a sense of mass.

MSource LandMellow UI

Why it stands out

A button that translates toward the cursor with spring physics, with the label counter-drifting to create a sense of mass.

Prompt

You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11 (install: `pnpm add motion`)

If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.

Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.

Copy-paste these files:

File location: components/magnetic-button-demo.tsx

File content: "use client";

import React from "react";
import { MagneticButton } from "../mellow/magnetic-button";

export default function MagneticButtonDemo() {
  return (
    <div className="flex flex-col items-center gap-5 p-4 sm:gap-8 sm:p-8">
      <div className="flex flex-wrap items-center justify-center gap-6 sm:gap-10">
        <MagneticButton>Explore</MagneticButton>
        <MagneticButton>Connect</MagneticButton>
        <MagneticButton variant="accent">Attract</MagneticButton>
        <MagneticButton variant="destructive">Delete</MagneticButton>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-10">
        <MagneticButton strength={0.7}>Strong</MagneticButton>
        <MagneticButton variant="accent" strength={0.2}>Subtle</MagneticButton>
        <MagneticButton disabled>Inert</MagneticButton>
      </div>
      <p className="text-[rgba(var(--ink-rgb),0.35)] text-[0.8125rem]">
        Hover to feel the pull · press to snap back
      </p>
    </div>
  );
}



File location: components/mellow/magnetic-button.tsx

File content: "use client";

import React, {
  useRef,
  useCallback,
  type ButtonHTMLAttributes,
} from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
} from "motion/react";

export interface MagneticButtonProps
  extends Omit<
    ButtonHTMLAttributes<HTMLButtonElement>,
    | "onDrag"
    | "onDragStart"
    | "onDragEnd"
    | "onAnimationStart"
    | "onAnimationEnd"
  > {
  children: React.ReactNode;
  variant?: "default" | "accent" | "destructive";
  strength?: number;
}

export function MagneticButton({
  children,
  variant = "default",
  strength = 0.45,
  className,
  style,
  disabled,
  onMouseMove,
  onMouseLeave,
  onMouseDown,
  onMouseUp,
  ...props
}: MagneticButtonProps) {
  const ref = useRef<HTMLButtonElement>(null);

  const mx = useMotionValue(0);
  const my = useMotionValue(0);
  const sc = useMotionValue(1);

  const springCfg = { stiffness: 180, damping: 16, mass: 0.9 };
  const springX = useSpring(mx, springCfg);
  const springY = useSpring(my, springCfg);
  const springScale = useSpring(sc, { stiffness: 380, damping: 26 });

  const labelX = useTransform(springX, (v) => -v * 0.3);
  const labelY = useTransform(springY, (v) => -v * 0.3);

  const isAccent = variant === "accent";
  const isDestructive = variant === "destructive";

  const handleMouseMove = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      if (!ref.current || disabled) return;
      const rect = ref.current.getBoundingClientRect();
      mx.set((e.clientX - (rect.left + rect.width / 2)) * strength);
      my.set((e.clientY - (rect.top + rect.height / 2)) * strength);
      onMouseMove?.(e);
    },
    [disabled, strength, mx, my, onMouseMove]
  );

  const handleMouseLeave = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      mx.set(0);
      my.set(0);
      sc.set(1);
      onMouseLeave?.(e);
    },
    [mx, my, sc, onMouseLeave]
  );

  const handleMouseDown = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      if (!disabled) {
        mx.set(0);
        my.set(0);
        sc.set(0.93);
      }
      onMouseDown?.(e);
    },
    [disabled, mx, my, sc, onMouseDown]
  );

  const handleMouseUp = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      sc.set(1);
      onMouseUp?.(e);
    },
    [sc, onMouseUp]
  );

  return (
    <motion.button
      ref={ref}
      disabled={disabled}
      className={[
        "relative inline-flex items-center justify-center px-5 py-[0.6rem] rounded-[8px]",
        "[font-family:var(--font-sans)] text-sm font-medium tracking-[-0.01em] select-none",
        disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
        isAccent
          ? "border border-[oklch(0.62_0.25_250/0.6)] bg-[oklch(0.50_0.22_250)] text-white"
          : isDestructive
          ? "border border-[oklch(0.62_0.2_25/0.6)] bg-[oklch(0.50_0.19_25)] text-white"
          : "border border-[rgba(var(--ink-rgb),0.14)] bg-[rgba(var(--ink-rgb),0.07)] text-[var(--ink)]",
        className,
      ].filter(Boolean).join(" ")}
      style={{
        x: springX,
        y: springY,
        scale: springScale,
        willChange: "transform",
        ...(style as object),
      }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      {...props}
    >
      <motion.span
        className="relative pointer-events-none"
        style={{ x: labelX, y: labelY }}
      >
        {children}
      </motion.span>
    </motion.button>
  );
}

export default MagneticButton;



Usage notes:
Add a MagneticButton from the mellow library. The button body springs toward the cursor position (translation, not rotation), while the label drifts slightly counter to the pull — creating a parallax sense of mass. On press it snaps to center and springs back. Use variant='default', variant='accent' for blue, or variant='destructive' for red delete/danger actions. The strength prop (default 0.45) controls pull intensity.

Discovery vocabulary

Related by governed terms

Continue comparing

More from Mellow UI