Magnetic Button

A button component with a magnetic pull effect that reacts to pointer movements.

Example

Installation

Copy and paste the source code into your project.

"use client";
 
import * as React from "react";
import {
  MotionValue,
  motion,
  useMotionValue,
  useTransform,
} from "framer-motion";
 
const mapRange = (
  inputLower: number,
  inputUpper: number,
  outputLower: number,
  outputUpper: number
) => {
  const INPUT_RANGE = inputUpper - inputLower;
  const OUTPUT_RANGE = outputUpper - outputLower;
 
  return (value: number) =>
    outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0);
};
 
const setTransform = (
  item: HTMLElement & EventTarget,
  event: React.PointerEvent,
  x: MotionValue,
  y: MotionValue,
  rangeOffset: number
) => {
  const bounds = item.getBoundingClientRect();
  const relativeX = event.clientX - bounds.left;
  const relativeY = event.clientY - bounds.top;
  const xRange = mapRange(0, bounds.width, -1, 1)(relativeX);
  const yRange = mapRange(0, bounds.height, -1, 1)(relativeY);
  x.set(xRange * rangeOffset);
  y.set(yRange * rangeOffset);
};
 
type Props = {
  children?: React.ReactNode;
  rangeOffset?: number;
  fullWidth?: boolean;
};
 
/**
 * MagneticButton component.
 *
 * A button component with a magnetic pull effect that reacts to pointer movements.
 *
 * @param {React.ReactNode} [children] - The content of the button.
 * @param {number} [rangeOffset=30] - The range offset for the magnetic effect.
 *
 * @note The magnetic effect of the button relies on the button's `getBoundingClientRect` method.
 * This means that the effect is based on the button's dimensions and position within the viewport.
 * Be aware that changes in the button's size or its parent container may affect the magnetic effect's behavior.
 */
export const MagneticButton = ({
  children,
  rangeOffset = 20,
  fullWidth = false,
}: Props) => {
  const x = useMotionValue(0);
  const y = useMotionValue(0);
 
  const spanOffset = 0.2;
  const textX = useTransform(x, (latest) => latest * spanOffset);
  const textY = useTransform(y, (latest) => latest * spanOffset);
 
  const cssVariables = {
    transitionProperty: "all",
    transitionDuration: "1000ms",
    transitionTimingFunction:
      "linear(0, 0.2178 2.1%, 1.1144 8.49%, 1.2959 10.7%, 1.3463 11.81%, 1.3705 12.94%, 1.3726, 1.3643 14.48%, 1.3151 16.2%, 1.0317 21.81%, 0.941 24.01%, 0.8912 25.91%, 0.8694 27.84%, 0.8698 29.21%, 0.8824 30.71%, 1.0122 38.33%, 1.0357, 1.046 42.71%, 1.0416 45.7%, 0.9961 53.26%, 0.9839 57.54%, 0.9853 60.71%, 1.0012 68.14%, 1.0056 72.24%, 0.9981 86.66%, 1)",
  } as React.CSSProperties;
 
  return (
    <motion.button
      onPointerMove={(event) => {
        const item = event.currentTarget;
        setTransform(item, event, x, y, rangeOffset);
      }}
      onPointerLeave={(_) => {
        x.set(0);
        y.set(0);
      }}
      whileTap={{ scale: 0.95 }}
      style={{
        x,
        y,
        ...cssVariables,
      }}
      className={`flex items-center justify-center ${fullWidth && "w-full"}`}
    >
      <motion.div
        style={{ x: textX, y: textY, ...cssVariables }}
        className={`relative ${fullWidth && "w-full"}`}
      >
        {children}
      </motion.div>
    </motion.button>
  );
};

    Update the import paths to match your project setup.

    More Examples

    Magnetic Button Socials

    Props

    PropTypeDefaultDescription
    childrennode-The content to be displayed inside the button. This is usually a text or an element.
    rangeOffsetnumber15The intensity of the magnetic pull effect. A higher value increases the movement range of the button.

    Notes

    • The magnetic effect of the button relies on the button's getBoundingClientRect method. This means that the effect is based on the button's dimensions and position within the viewport. Be aware that changes in the button's size or its parent container may affect the magnetic effect's behavior.