Components

Magnetic

A component that creates a magnetic attraction effect, making elements follow the cursor with smooth spring animations.

Last updated on

Edit on GitHub
Open in
import { Magnetic } from "@/components/ui/magnetic";
 
export function MagneticDemo() {
  return (
    <div className="flex min-h-[300px] items-center justify-center">
      <Magnetic>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg transition-colors hover:bg-primary/90">
          Hover Me
        </button>
      </Magnetic>
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://jolyui.dev/r/magnetic"

Manual

Install the following dependencies:

npm install motion

Copy and paste the following code into your project.

"use client";
 
import {
  motion,
  type SpringOptions,
  useMotionValue,
  useSpring,
} from "motion/react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
 
const SPRING_CONFIG = { stiffness: 26.7, damping: 4.1, mass: 0.2 };
 
export type MagneticProps = {
  children: React.ReactNode;
  intensity?: number;
  range?: number;
  actionArea?: "self" | "parent" | "global";
  springOptions?: SpringOptions;
};
 
export function Magnetic({
  children,
  intensity = 0.6,
  range = 100,
  actionArea = "self",
  springOptions = SPRING_CONFIG,
}: MagneticProps) {
  const [isHovered, setIsHovered] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
 
  const x = useMotionValue(0);
  const y = useMotionValue(0);
 
  const springX = useSpring(x, springOptions);
  const springY = useSpring(y, springOptions);
 
  useEffect(() => {
    const calculateDistance = (e: MouseEvent) => {
      if (ref.current) {
        const rect = ref.current.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;
        const distanceX = e.clientX - centerX;
        const distanceY = e.clientY - centerY;
 
        const absoluteDistance = Math.sqrt(distanceX ** 2 + distanceY ** 2);
 
        if (isHovered && absoluteDistance <= range) {
          const scale = 1 - absoluteDistance / range;
          x.set(distanceX * intensity * scale);
          y.set(distanceY * intensity * scale);
        } else {
          x.set(0);
          y.set(0);
        }
      }
    };
 
    document.addEventListener("mousemove", calculateDistance);
 
    return () => {
      document.removeEventListener("mousemove", calculateDistance);
    };
  }, [isHovered, intensity, range, x.set, y.set]);
 
  useEffect(() => {
    if (actionArea === "parent" && ref.current?.parentElement) {
      const parent = ref.current.parentElement;
 
      const handleParentEnter = () => setIsHovered(true);
      const handleParentLeave = () => setIsHovered(false);
 
      parent.addEventListener("mouseenter", handleParentEnter);
      parent.addEventListener("mouseleave", handleParentLeave);
 
      return () => {
        parent.removeEventListener("mouseenter", handleParentEnter);
        parent.removeEventListener("mouseleave", handleParentLeave);
      };
    } else if (actionArea === "global") {
      setIsHovered(true);
    }
  }, [actionArea]);
 
  const handleMouseEnter = () => {
    if (actionArea === "self") {
      setIsHovered(true);
    }
  };
 
  const handleMouseLeave = () => {
    if (actionArea === "self") {
      setIsHovered(false);
      x.set(0);
      y.set(0);
    }
  };
 
  return (
    <motion.div
      ref={ref}
      onMouseEnter={actionArea === "self" ? handleMouseEnter : undefined}
      onMouseLeave={actionArea === "self" ? handleMouseLeave : undefined}
      style={{
        x: springX,
        y: springY,
      }}
    >
      {children}
    </motion.div>
  );
}

Layout

import { Magnetic } from "@/components/ui/magnetic";

<Magnetic>
  <button>Hover Me</button>
</Magnetic>

Examples

Intensity

Control the strength of the magnetic effect with the intensity prop. Higher values create stronger attraction.

import { Magnetic } from "@/components/ui/magnetic";
 
export function MagneticIntensityDemo() {
  return (
    <div className="flex min-h-[300px] flex-wrap items-center justify-center gap-8">
      <Magnetic intensity={0.3}>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
          Low (0.3)
        </button>
      </Magnetic>
      <Magnetic intensity={0.6}>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
          Medium (0.6)
        </button>
      </Magnetic>
      <Magnetic intensity={1.2}>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
          High (1.2)
        </button>
      </Magnetic>
    </div>
  );
}

Range

Adjust the activation distance with the range prop. This defines how far from the element the effect begins.

import { Magnetic } from "@/components/ui/magnetic";
 
export function MagneticRangeDemo() {
  return (
    <div className="flex min-h-[300px] flex-wrap items-center justify-center gap-8">
      <Magnetic range={50}>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
          Small Range (50px)
        </button>
      </Magnetic>
      <Magnetic range={100}>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
          Medium Range (100px)
        </button>
      </Magnetic>
      <Magnetic range={200}>
        <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
          Large Range (200px)
        </button>
      </Magnetic>
    </div>
  );
}

Action Area

Define where the magnetic effect should be triggered using the actionArea prop.

import { Magnetic } from "@/components/ui/magnetic";
 
export function MagneticActionAreaDemo() {
  return (
    <div className="flex min-h-[400px] flex-col items-center justify-center gap-12">
      <div className="flex flex-col items-center gap-2">
        <p className="text-muted-foreground text-sm">Self (hover the button)</p>
        <Magnetic actionArea="self">
          <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
            Self Action Area
          </button>
        </Magnetic>
      </div>
 
      <div className="flex flex-col items-center gap-2">
        <p className="text-muted-foreground text-sm">
          Parent (hover the container)
        </p>
        <div className="rounded-lg border-2 border-muted-foreground/30 border-dashed p-8">
          <Magnetic actionArea="parent">
            <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
              Parent Action Area
            </button>
          </Magnetic>
        </div>
      </div>
 
      <div className="flex flex-col items-center gap-2">
        <p className="text-muted-foreground text-sm">Global (always active)</p>
        <Magnetic actionArea="global">
          <button className="rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground text-sm shadow-lg">
            Global Action Area
          </button>
        </Magnetic>
      </div>
    </div>
  );
}

Cards Example

A practical example showing the magnetic effect applied to card components.

import { Magnetic } from "@/components/ui/magnetic";
 
export function MagneticCardsDemo() {
  const cards = [
    { title: "Design", description: "Beautiful UI components", icon: "🎨" },
    {
      title: "Development",
      description: "Clean and efficient code",
      icon: "💻",
    },
    { title: "Animation", description: "Smooth interactions", icon: "✨" },
  ];
 
  return (
    <div className="flex min-h-[400px] flex-wrap items-center justify-center gap-6 p-8">
      {cards.map((card, index) => (
        <Magnetic key={index} intensity={0.4} range={120}>
          <div className="flex h-48 w-56 flex-col items-center justify-center gap-3 rounded-xl border bg-card p-6 shadow-lg transition-shadow hover:shadow-xl">
            <span className="text-4xl">{card.icon}</span>
            <h3 className="font-semibold text-lg">{card.title}</h3>
            <p className="text-center text-muted-foreground text-sm">
              {card.description}
            </p>
          </div>
        </Magnetic>
      ))}
    </div>
  );
}

API Reference

Magnetic

The main magnetic wrapper component that applies smooth cursor-following animations to its children.

Prop

Type

How is this guide?

On this page