interaction·free
Cursor Follower
Lagging custom cursor that grows over interactive elements.
Installation
terminal
pnpm add gsap @gsap/reactThen copy the component below into src/components/motion/cursor-follower.tsx. It expects @/lib/gsap (plugin registration) and @/lib/utils (cn).
Source
src/components/motion/cursor-follower.tsx
"use client";
import { useRef } from "react";
import { gsap, useGSAP } from "@/lib/gsap";
import { cn } from "@/lib/utils";
export interface CursorFollowerProps {
size?: number;
className?: string;
/** Lag in seconds. */
lag?: number;
/** CSS selector; hovering matching elements grows the cursor. */
hoverSelector?: string;
hoverScale?: number;
}
export function CursorFollower({
size = 24,
className,
lag = 0.25,
hoverSelector = "a, button, [data-cursor]",
hoverScale = 3,
}: CursorFollowerProps) {
const ref = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(pointer: coarse)").matches) return;
gsap.set(el, { xPercent: -50, yPercent: -50, opacity: 0 });
const x = gsap.quickTo(el, "x", { duration: lag, ease: "power3" });
const y = gsap.quickTo(el, "y", { duration: lag, ease: "power3" });
const move = (e: MouseEvent) => {
x(e.clientX);
y(e.clientY);
gsap.to(el, { opacity: 1, duration: 0.3 });
};
const over = (e: MouseEvent) => {
const t = (e.target as Element).closest?.(hoverSelector);
gsap.to(el, { scale: t ? hoverScale : 1, duration: 0.35, ease: "power3.out" });
};
window.addEventListener("mousemove", move);
document.addEventListener("mouseover", over);
return () => {
window.removeEventListener("mousemove", move);
document.removeEventListener("mouseover", over);
};
},
{ scope: ref, dependencies: [lag, hoverSelector, hoverScale] }
);
return (
<div
ref={ref}
aria-hidden
className={cn("pointer-events-none fixed left-0 top-0 z-[9999] rounded-full mix-blend-difference bg-white", className)}
style={{ width: size, height: size }}
/>
);
}