Cursor Trail
Fading gradient trail that follows the mouse with smooth physics easing
animationcursortrailinteractivemouse
Preview
Source Code
"use client";
import { useRef, useEffect, useCallback } from "react";
interface Point {
x: number;
y: number;
}
interface CursorTrailProps {
children?: React.ReactNode;
color?: string;
trailLength?: number;
dotSize?: number;
className?: string;
}
export default function CursorTrail({
children,
color = "#38bdf8",
trailLength = 20,
dotSize = 8,
className = "",
}: CursorTrailProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const pointsRef = useRef<Point[]>([]);
const mouseRef = useRef<Point>({ x: -100, y: -100 });
const animationRef = useRef<number>(0);
const isInsideRef = useRef(false);
const initPoints = useCallback(() => {
pointsRef.current = Array.from({ length: trailLength }, () => ({
x: -100,
y: -100,
}));
}, [trailLength]);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let cssWidth = 0;
let cssHeight = 0;
const resize = () => {
const dpr = window.devicePixelRatio || 1;
cssWidth = container.clientWidth;
cssHeight = container.clientHeight;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
resize();
initPoints();
window.addEventListener("resize", resize);
const handleMouseMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
mouseRef.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
isInsideRef.current = true;
};
const handleMouseLeave = () => {
isInsideRef.current = false;
};
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseleave", handleMouseLeave);
const animate = () => {
ctx.clearRect(0, 0, cssWidth, cssHeight);
const points = pointsRef.current;
const mouse = mouseRef.current;
// Lead point follows mouse with easing
points[0].x += (mouse.x - points[0].x) * 0.35;
points[0].y += (mouse.y - points[0].y) * 0.35;
// Each subsequent point follows the one before it
for (let i = 1; i < points.length; i++) {
points[i].x += (points[i - 1].x - points[i].x) * 0.3;
points[i].y += (points[i - 1].y - points[i].y) * 0.3;
}
if (isInsideRef.current || points[points.length - 1].x > 0) {
// Draw trail dots
for (let i = points.length - 1; i >= 0; i--) {
const t = 1 - i / points.length;
const size = dotSize * t;
const alpha = t * 0.8;
if (size < 0.5) continue;
// Glow
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, size * 2.5, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = alpha * 0.1;
ctx.fill();
// Core dot
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = alpha;
ctx.fill();
}
}
ctx.globalAlpha = 1;
animationRef.current = requestAnimationFrame(animate);
};
animate();
return () => {
cancelAnimationFrame(animationRef.current);
window.removeEventListener("resize", resize);
container.removeEventListener("mousemove", handleMouseMove);
container.removeEventListener("mouseleave", handleMouseLeave);
};
}, [color, dotSize, initPoints]);
return (
<div ref={containerRef} className={`relative overflow-hidden ${className}`}>
<canvas ref={canvasRef} className="pointer-events-none absolute inset-0 z-20" />
<div className="relative z-10">{children}</div>
</div>
);
}