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>
  );
}