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"> </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
Prop | Type | Default | Description |
---|---|---|---|
text | string | — | The text to be displayed and affected by the magnetic effect. |
className | string | '' | Additional Tailwind or custom class names for styling the container. |
colorAnimation | boolean | false | Enable or disable the color transition on hover. |
hoverColor | string | 'rgb(255, 64, 129)' | Color of the text when hovered (only if colorAnimation is true). |
defaultColor | string | 'rgb(255, 255, 255)' | Base color of the text when not hovered. |
enableX | boolean | false | Enable magnetic horizontal movement along the X axis. |
enableY | boolean | false | Enable magnetic vertical movement along the Y axis. |