Command Palette

Search for a command to run...

Magnetic Text Effect

A magnetic text effect that creates a visually appealing animation when the user hovers over the text. The text appears to be magnetically attracted to the cursor, creating an engaging interaction.

MAGNATEXT
 Magnetic text hover effect 

Examples

Colored Animation

Hover over the text to see it change color with magnetic effect while following your cursor.

 See the color change  

Enable X and Y Push

Hover over the text to see the text go in opposite direction from your cursor on both X and Y axis.

 Move your cursor to feel the push 

Installation

Install required dependencies

npm i motion clsx tailwind-merge

Copy the following source code

"use client";

import { cn } from "@/lib/utils";
import { motion, useMotionValue, useTransform, useMotionTemplate, useSpring } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { useTheme } from "next-themes";

type MousePosition = { x: number; y: number } | null;

interface MagneticTextProps {
  text: string;
  className?: string;
  colorAnimation?: boolean;
  lightModeColor?: string;
  darkModeColor?: string;
  lightModeHoverColor?: string;
  darkModeHoverColor?: string;
  enableX?: boolean;
  enableY?: boolean;
}

const MagneticText = ({
  text,
  className = '',
  lightModeColor = "rgb(23, 23, 23)",      // Default light mode color
  lightModeHoverColor = "rgb(255, 64, 129)", // Default light mode hover color
  darkModeColor = "rgb(255, 255, 255)",     // Default dark mode color
  darkModeHoverColor = "rgb(255, 64, 129)",// Default dark mode hover color
  colorAnimation = false,
  enableX = false,
  enableY = false
}: MagneticTextProps) => {
  const letters = text.split('');
  const containerRef = useRef<HTMLDivElement>(null);
  const [mousePosition, setMousePosition] = useState<MousePosition>(null);
  const { resolvedTheme } = useTheme();

  const currentTheme = resolvedTheme || 'dark';
  const defaultColor = currentTheme === 'dark' ? darkModeColor : lightModeColor;
  const hoverColor = currentTheme === 'dark' ? darkModeHoverColor : lightModeHoverColor;

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    setMousePosition({ x: e.clientX, y: e.clientY });
  };

  const handleMouseLeave = () => {
    setMousePosition(null);
  };

  return (
    <div
      ref={containerRef}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      className={cn(
        "font-inter text-8xl leading-[1em] tracking-wider relative cursor-default flex font-light",
        className
      )}
    >
      {letters.map((letter, i) => (
        <MagneticLetter
          key={i}
          letter={letter}
          mousePosition={mousePosition}
          colorAnimation={colorAnimation}
          hoverColor={hoverColor}
          defaultColor={defaultColor}
          enableX={enableX}
          enableY={enableY}
        />
      ))}
    </div>
  );
};

const MagneticLetter = ({
  letter,
  mousePosition,
  hoverColor,
  defaultColor,
  colorAnimation,
  enableX,
  enableY
}: {
  letter: string;
  mousePosition: MousePosition;
  hoverColor: string;
  defaultColor: string;
  colorAnimation: boolean;
  enableX?: boolean;
  enableY?: boolean;
}) => {
  const ref = useRef<HTMLSpanElement>(null);

  const initialStrokeWidth = 1; 

  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const scaleX = useMotionValue(1);
  const scaleY = useMotionValue(1);
  const stroke = useMotionValue(initialStrokeWidth);
  const padding = useMotionValue(0);
  const colorStrength = useMotionValue(0);

  const springConfig = { stiffness: 1000, damping: 100 };

  const sx = useSpring(x, springConfig);
  const sy = useSpring(y, springConfig);
  const sScaleX = useSpring(scaleX, springConfig);
  const sScaleY = useSpring(scaleY, springConfig);
  const sStroke = useSpring(stroke, springConfig);
  const sPadding = useSpring(padding, springConfig);
  const sColorStrength = useSpring(colorStrength, springConfig);

  const textStrokeTemplate = useMotionTemplate`${sStroke}px currentColor`;
  const padInlineTemplate = useMotionTemplate`${sPadding}px`;

  // Call useTransform unconditionally
  const animatedColor = useTransform(sColorStrength, [0, 1], [defaultColor, hoverColor]);
  // Use the result of the hook conditionally
  const finalTransformedColor = colorAnimation ? animatedColor : defaultColor;

  // Constants for effect calculations (defined within component scope)
  const baseFontSize = 60;
  const baseMoveFactor = 0.5;
  const baseScaleDistortion = 0.1;
  const baseStrokeWidthForEffect = 1; // Renamed from original baseStrokeWidth to avoid confusion
  const baseStrokeFactor = 8;
  const basePaddingFactor = 5;
  const baseInteractionRadiusFactor = 1.5;

  useEffect(() => {
    if (!ref.current) {
      return;
    }

    const styles = window.getComputedStyle(ref.current);
    const fontSizeString = styles.fontSize;
    const fontSize = parseFloat(fontSizeString);

    const safeFontSize = fontSize > 0 ? fontSize : baseFontSize;
    const fontSizeScaleFactor = safeFontSize / baseFontSize;

    const dynamicMaxDistance = safeFontSize * baseInteractionRadiusFactor;
    const moveMultiplier = baseMoveFactor;
    const scaleMultiplier = baseScaleDistortion;
    const dynamicBaseStroke = baseStrokeWidthForEffect * fontSizeScaleFactor;
    const strokeMultiplier = baseStrokeFactor * fontSizeScaleFactor;
    const paddingMultiplier = basePaddingFactor * fontSizeScaleFactor;

    const { left, top, width, height } = ref.current.getBoundingClientRect();
    const cx = left + width / 2;
    const cy = top + height / 2;

    if (mousePosition) {
      const dx = mousePosition.x - cx;
      const dy = mousePosition.y - cy;
      const dist = Math.hypot(dx, dy);

      if (dist < dynamicMaxDistance) {
        const strength = Math.max(0, Math.min(1, (dynamicMaxDistance - dist) / dynamicMaxDistance));
        x.set(-dx * moveMultiplier * strength);
        y.set(-dy * moveMultiplier * strength);
        scaleX.set(1 + strength * scaleMultiplier);
        scaleY.set(1 - strength * scaleMultiplier);
        stroke.set(dynamicBaseStroke + strength * strokeMultiplier);
        padding.set(strength * paddingMultiplier);

        if (colorAnimation) {
          colorStrength.set(strength * 1.5);
        } else {
          colorStrength.set(0); // Reset if colorAnimation is false
        }
      } else {
        x.set(0);
        y.set(0);
        scaleX.set(1);
        scaleY.set(1);
        stroke.set(dynamicBaseStroke);
        padding.set(0);
        colorStrength.set(0);
      }
    } else {
      x.set(0);
      y.set(0);
      scaleX.set(1);
      scaleY.set(1);
      stroke.set(dynamicBaseStroke); 
      padding.set(0);
      colorStrength.set(0);
    }
  }, [
      mousePosition,
      letter,
      colorAnimation,
      x, y, scaleX, scaleY, stroke, padding, colorStrength,
    ]);

  // Conditional return for space characters (AFTER all hooks have been called)
  if (letter === ' ') {
    return <span className="inline-block">&nbsp;</span>;
  }

  return (
    <motion.span
      ref={ref}
      style={{
        ...(enableX && { x: sx }),
        ...(enableY && { y: sy }),
        scaleX: sScaleX,
        scaleY: sScaleY,
        paddingInline: padInlineTemplate,
        WebkitTextStroke: textStrokeTemplate,
        display: "inline-block",
        color: finalTransformedColor,
      }}
      className="will-change-transform"
      transition={{
        type: "tween",
        ease: "easeOut",
        duration: 0.05,
      }}
    >
      {letter}
    </motion.span>
  );
};

export default MagneticText;

Now paste it into your project and thats it !

Props

PropTypeDefaultDescription
textstringThe text to be displayed and affected by the magnetic effect.
classNamestring''Additional Tailwind or custom class names for styling the container.
colorAnimationbooleanfalseEnable or disable the color transition on hover.
hoverColorstring'rgb(255, 64, 129)'Color of the text when hovered (only if colorAnimation is true).
defaultColorstring'rgb(255, 255, 255)'Base color of the text when not hovered.
enableXbooleanfalseEnable magnetic horizontal movement along the X axis.
enableYbooleanfalseEnable magnetic vertical movement along the Y axis.