Mouse Trail

PreviousNext

A cursor-trail animation that leaves small dots behind the mouse as it moves, with the dots fading out sequentially to create a snake-tail-like effect. Built with Tailwind CSS, React and Framer Motion.

Docs
bunduicomponent

Preview

Loading preview…
examples/motion/animations/mouse-trail/01/mouse-trail.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "motion/react";

interface Dot {
  id: number;
  x: number;
  y: number;
}

interface MouseTrailProps {
  dotColor?: string;
  dotSize?: number;
  spacing?: number;
  trailLength?: number;
  fadeDuration?: number;
}

export const MouseTrail = ({
  dotColor = "#a855f7",
  dotSize = 8,
  spacing = 20,
  trailLength = 20,
  fadeDuration = 1000
}: MouseTrailProps) => {
  const [dots, setDots] = useState<Dot[]>([]);
  const lastPositionRef = useRef({ x: 0, y: 0 });
  const dotIdRef = useRef(0);

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      const { clientX: x, clientY: y } = e;
      const lastPos = lastPositionRef.current;

      // Calculate distance from last dot
      const distance = Math.sqrt(Math.pow(x - lastPos.x, 2) + Math.pow(y - lastPos.y, 2));

      // Only add dot if we've moved far enough
      if (distance >= spacing) {
        const newDot: Dot = {
          id: dotIdRef.current++,
          x,
          y
        };

        setDots((prevDots) => {
          const updatedDots = [...prevDots, newDot];
          // Keep only the most recent dots based on trail length
          return updatedDots.slice(-trailLength);
        });

        lastPositionRef.current = { x, y };

        // Remove dot after fade duration
        setTimeout(() => {
          setDots((prevDots) => prevDots.filter((d) => d.id !== newDot.id));
        }, fadeDuration);
      }
    };

    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, [spacing, trailLength, fadeDuration]);

  return (
    <div className="pointer-events-none fixed inset-0 z-50">
      <AnimatePresence>
        {dots.map((dot) => (
          <motion.div
            key={dot.id}
            className="absolute rounded-full"
            initial={{
              left: dot.x - dotSize / 2,
              top: dot.y - dotSize / 2,
              opacity: 1,
              scale: 0
            }}
            animate={{
              opacity: 1,
              scale: 1
            }}
            exit={{
              opacity: 0,
              scale: 0.5
            }}
            transition={{
              duration: fadeDuration / 1000,
              ease: "easeOut"
            }}
            style={{
              width: dotSize,
              height: dotSize,
              backgroundColor: dotColor
            }}
          />
        ))}
      </AnimatePresence>
    </div>
  );
};

Installation

npx shadcn@latest add @bundui/mouse-trail

Usage

import { MouseTrail } from "@/components/mouse-trail"
<MouseTrail />