Morphing Cursor
A magnetic text effect component with a morphing cursor that reveals alternate text on hover. Creates an engaging interactive experience with smooth animations.
Last updated on
import { MagneticText } from "@/components/ui/morphing-cursor";
export function MorphingCursorDemo() {
return (
<div className="flex min-h-[300px] items-center justify-center">
<MagneticText text="CREATIVE" hoverText="EXPLORE" />
</div>
);
}Installation
CLI
npx shadcn@latest add "https://jolyui.dev/r/morphing-cursor"Manual
Copy and paste the following code into your project.
"use client";
import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
interface MagneticTextProps {
text: string;
hoverText?: string;
className?: string;
}
export function MagneticText({
text = "CREATIVE",
hoverText = "EXPLORE",
className,
}: MagneticTextProps) {
const containerRef = useRef<HTMLDivElement>(null);
const circleRef = useRef<HTMLDivElement>(null);
const innerTextRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const mousePos = useRef({ x: 0, y: 0 });
const currentPos = useRef({ x: 0, y: 0 });
const animationFrameRef = useRef<number | undefined>(undefined);
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
setContainerSize({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight,
});
}
};
updateSize();
window.addEventListener("resize", updateSize);
return () => window.removeEventListener("resize", updateSize);
}, []);
useEffect(() => {
const lerp = (start: number, end: number, factor: number) =>
start + (end - start) * factor;
const animate = () => {
currentPos.current.x = lerp(
currentPos.current.x,
mousePos.current.x,
0.15,
);
currentPos.current.y = lerp(
currentPos.current.y,
mousePos.current.y,
0.15,
);
if (circleRef.current) {
circleRef.current.style.transform = `translate(${currentPos.current.x}px, ${currentPos.current.y}px) translate(-50%, -50%)`;
}
if (innerTextRef.current) {
innerTextRef.current.style.transform = `translate(${-currentPos.current.x}px, ${-currentPos.current.y}px)`;
}
animationFrameRef.current = requestAnimationFrame(animate);
};
animationFrameRef.current = requestAnimationFrame(animate);
return () => {
if (animationFrameRef.current)
cancelAnimationFrame(animationFrameRef.current);
};
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
mousePos.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
}, []);
const handleMouseEnter = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
mousePos.current = { x, y };
currentPos.current = { x, y };
setIsHovered(true);
},
[],
);
const handleMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
return (
// biome-ignore lint/a11y/noStaticElementInteractions: This is a decorative visual effect that tracks mouse position for aesthetic purposes only
<div
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(
"relative inline-flex cursor-none select-none items-center justify-center",
className,
)}
>
{/* Base text layer - original text */}
<span className="font-bold text-5xl text-foreground tracking-tighter tracking-wide">
{text}
</span>
<div
ref={circleRef}
className="pointer-events-none absolute top-0 left-0 overflow-hidden rounded-full bg-foreground"
style={{
width: isHovered ? 150 : 0,
height: isHovered ? 150 : 0,
transition:
"width 0.5s cubic-bezier(0.33, 1, 0.68, 1), height 0.5s cubic-bezier(0.33, 1, 0.68, 1)",
willChange: "transform, width, height",
}}
>
<div
ref={innerTextRef}
className="absolute flex items-center justify-center"
style={{
width: containerSize.width,
height: containerSize.height,
top: "50%",
left: "50%",
willChange: "transform",
}}
>
<span className="whitespace-nowrap font-bold text-5xl text-background tracking-tighter tracking-wide">
{hoverText}
</span>
</div>
</div>
</div>
);
}Layout
import { MagneticText } from "@/components/ui/morphing-cursor";
<MagneticText text="CREATIVE" hoverText="EXPLORE" />Examples
Custom Text
Use different text combinations to create unique interactive experiences.
import { MagneticText } from "@/components/ui/morphing-cursor";
export function MorphingCursorCustomDemo() {
return (
<div className="flex min-h-[300px] flex-wrap items-center justify-center gap-12">
<MagneticText text="DESIGN" hoverText="CREATE" />
<MagneticText text="BUILD" hoverText="LAUNCH" />
</div>
);
}With Container Styling
Add custom styling to the container using the className prop.
import { MagneticText } from "@/components/ui/morphing-cursor";
export function MorphingCursorStyledDemo() {
return (
<div className="flex min-h-[300px] items-center justify-center">
<MagneticText
text="HOVER ME"
hoverText="MAGIC"
className="rounded-2xl bg-muted/50 p-8"
/>
</div>
);
}API Reference
MagneticText
The main component that creates a magnetic cursor effect revealing alternate text on hover.
Prop
Type
How is this guide?
Magnetic
Magnetic hover effect component for React. Elements smoothly follow cursor with spring physics. Perfect for buttons, cards, and interactive UI.
Video Player
Custom React video player with playback controls, volume slider, fullscreen mode, keyboard shortcuts, and progress bar. Fully accessible and styleable.