Skip to content
All Taste components

Animated buttons

Magnetic button

A button that leans toward the pointer as it comes near, then springs back when it leaves.

8 pull directions · label parallax · spring return

Move the pointer around the button
"use client";

import { useRef } from "react";
import { cn } from "@/lib/utils";

interface MagneticButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** How far the button follows the pointer, 0–1. */
  strength?: number;
}

export function MagneticButton({ strength = 0.3, className, children, ...props }: MagneticButtonProps) {
  const ref = useRef<HTMLButtonElement>(null);

  function follow(e: React.PointerEvent<HTMLSpanElement>) {
    const el = ref.current;
    if (!el || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const r = el.getBoundingClientRect();
    el.style.setProperty("--mx", (e.clientX - (r.left + r.width / 2)) * strength + "px");
    el.style.setProperty("--my", (e.clientY - (r.top + r.height / 2)) * strength + "px");
  }

  function release() {
    ref.current?.style.setProperty("--mx", "0px");
    ref.current?.style.setProperty("--my", "0px");
  }

  return (
    // The padded wrapper is the "field": the pull starts before the pointer reaches the button.
    <span className="inline-block p-6" onPointerMove={follow} onPointerLeave={release}>
      <button
        ref={ref}
        type="button"
        className={cn(
          "inline-flex h-11 items-center justify-center rounded bg-primary px-5 text-[15px] font-medium text-primary-fg hover:bg-primary-hover",
          "translate-x-[var(--mx,0px)] translate-y-[var(--my,0px)] transition duration-500 ease-spring motion-reduce:transition-none",
          "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
          className,
        )}
        {...props}
      >
        <span className="inline-flex translate-x-[calc(var(--mx,0px)*.5)] translate-y-[calc(var(--my,0px)*.5)] items-center gap-1.5 transition duration-500 ease-spring motion-reduce:transition-none [&_svg]:size-4">
          {children}
        </span>
      </button>
    </span>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.