Move your cursor over the demo
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Multiple trailing lines follow mouse with spring physics. Supports touch on mobile. Great for hero sections and interactive backgrounds.
/*
================================================================================
AI COMPONENT: TrailingCursor
================================================================================
SETUP:
1. Create file: components/TrailingCursor.tsx
2. Copy this entire code block into that file
3. Import: import TrailingCursor from "@/components/TrailingCursor"
4. No external dependencies required - pure React + Canvas
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default |
|-----------------|-------------------|----------------------------------|
| size | number | 400 |
| lineCount | number | 4 |
| pointCount | number | 20 |
| lineWidth | [number, number] | [3, 16] (min, max) |
| colors | string[] | Red gradient palette |
| backgroundColor | string | "transparent" |
| spring | number | 0.06 (0-1, higher = snappier) |
| friction | number | 0.85 (0-1, higher = more drag) |
| className | string | "" |
================================================================================
COLOR PALETTES
================================================================================
RED GRADIENT (default):
["#FF0844", "#FF4B6E", "#FF7B54", "#FFB26B"]
BLUE GRADIENT:
["#0066FF", "#00AAFF", "#00DDFF", "#66FFFF"]
PURPLE GRADIENT:
["#8B5CF6", "#A855F7", "#D946EF", "#F472B6"]
GREEN GRADIENT:
["#00FF88", "#22C55E", "#84CC16", "#FACC15"]
RAINBOW:
["#FF0844", "#FF7B54", "#22C55E", "#0066FF", "#8B5CF6"]
================================================================================
COMMON MODIFICATIONS
================================================================================
1. CHANGE COLORS:
<TrailingCursor colors={["#0066FF", "#00AAFF", "#00DDFF", "#66FFFF"]} />
2. MORE LINES:
<TrailingCursor lineCount={6} />
3. THICKER LINES:
<TrailingCursor lineWidth={[5, 24]} />
4. SNAPPIER MOVEMENT:
<TrailingCursor spring={0.1} friction={0.9} />
5. SMOOTHER/LAZIER:
<TrailingCursor spring={0.03} friction={0.75} />
6. FULLSCREEN:
const [size, setSize] = useState(window.innerWidth);
<TrailingCursor size={size} className="fixed inset-0 z-0" />
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useRef, useEffect, useCallback } from "react";
// ============================================================================
// TYPES
// ============================================================================
export interface TrailingCursorProps {
/** Size of the canvas (default: 400) */
size?: number;
/** Number of trailing lines (default: 4) */
lineCount?: number;
/** Number of points per line (default: 20) */
pointCount?: number;
/** Line width range [min, max] (default: [3, 16]) */
lineWidth?: [number, number];
/** Custom gradient colors (default: red gradient) */
colors?: string[];
/** Background color (default: transparent) */
backgroundColor?: string;
/** Spring stiffness 0-1 (default: 0.06) */
spring?: number;
/** Friction 0-1 (default: 0.85) */
friction?: number;
/** Additional CSS classes */
className?: string;
}
// ============================================================================
// DEFAULT COLORS - Red Gradient Palette
// ============================================================================
const defaultColors = [
"#FF0844", // Bright red
"#FF4B6E", // Coral red
"#FF7B54", // Orange red
"#FFB26B", // Light orange
];
// ============================================================================
// TRAILING CURSOR COMPONENT
// ============================================================================
export default function TrailingCursor({
size = 400,
lineCount = 4,
pointCount = 20,
lineWidth = [3, 16],
colors = defaultColors,
backgroundColor = "transparent",
spring = 0.06,
friction = 0.85,
className = "",
}: TrailingCursorProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number | null>(null);
const stateRef = useRef<{
lines: Array<{
points: Array<{ x: number; y: number }>;
offset: { x: number; y: number };
velocity: { x: number; y: number };
spring: number;
friction: number;
color: string;
}>;
target: { x: number; y: number };
mouseSpeed: number;
targetMouseSpeed: number;
lastMousePos: { x: number; y: number };
isTouch: boolean;
} | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const width = size;
const height = size;
const centerX = width / 2;
const centerY = height / 2;
// Set canvas size with device pixel ratio
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);
// Initialize state
const lines: typeof stateRef.current extends { lines: infer L } | null
? L
: never = [];
for (let i = 0; i < lineCount; i++) {
const angle = (i / lineCount) * Math.PI * 2;
const radius = 0.2 * Math.min(width, height) * 0.15;
const offsetX =
Math.cos(angle) * radius + (Math.random() - 0.5) * radius * 0.5;
const offsetY =
Math.sin(angle) * radius + (Math.random() - 0.5) * radius * 0.5;
const points = Array(pointCount)
.fill(null)
.map(() => ({
x: centerX + offsetX,
y: centerY + offsetY,
}));
lines.push({
points,
offset: { x: offsetX, y: offsetY },
velocity: { x: 0, y: 0 },
spring: spring + (Math.random() - 0.5) * 0.02,
friction: friction + (Math.random() - 0.5) * 0.05,
color: colors[i % colors.length],
});
}
stateRef.current = {
lines,
target: { x: centerX, y: centerY },
mouseSpeed: 0,
targetMouseSpeed: 0,
lastMousePos: { x: centerX, y: centerY },
isTouch: false,
};
const state = stateRef.current;
// Smooth line drawing with gradient width
function drawLine(
points: Array<{ x: number; y: number }>,
color: string,
baseWidth: number,
) {
if (points.length < 2) return;
// Draw the line as connected segments with varying width
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[i + 1];
// Progress along the line (0 = head, 1 = tail)
const t = i / (points.length - 1);
// Width tapers at both ends
let widthMult = 1;
const edge = 0.15;
if (t < edge) {
widthMult = t / edge;
} else if (t > 1 - edge) {
widthMult = (1 - t) / edge;
}
const segmentWidth = Math.max(
lineWidth[0],
baseWidth * widthMult * (1 - t * 0.5),
);
// Create gradient for this segment
const gradient = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
// Parse color and add alpha based on position
const alpha = 0.8 - t * 0.6;
gradient.addColorStop(0, colorWithAlpha(color, alpha + 0.1));
gradient.addColorStop(1, colorWithAlpha(color, alpha));
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = gradient;
ctx.lineWidth = segmentWidth;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
}
// Draw glow at the head
const head = points[0];
const glowGradient = ctx.createRadialGradient(
head.x,
head.y,
0,
head.x,
head.y,
baseWidth * 2.5,
);
glowGradient.addColorStop(0, colorWithAlpha(color, 0.7));
glowGradient.addColorStop(0.4, colorWithAlpha(color, 0.3));
glowGradient.addColorStop(1, "transparent");
ctx.beginPath();
ctx.arc(head.x, head.y, baseWidth * 2.5, 0, Math.PI * 2);
ctx.fillStyle = glowGradient;
ctx.fill();
// Draw solid head dot
ctx.beginPath();
ctx.arc(head.x, head.y, baseWidth * 0.5, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
}
function colorWithAlpha(color: string, alpha: number): string {
// Convert hex to rgba
const hex = color.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, alpha))})`;
}
function animate() {
if (!state) return;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw background if set
if (backgroundColor !== "transparent") {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, width, height);
}
// Calculate mouse speed
const dx = state.target.x - state.lastMousePos.x;
const dy = state.target.y - state.lastMousePos.y;
const speed = Math.sqrt(dx * dx + dy * dy);
state.targetMouseSpeed = speed * 0.15;
state.mouseSpeed += (state.targetMouseSpeed - state.mouseSpeed) * 0.1;
state.lastMousePos = { ...state.target };
// Dynamic line width based on speed
const dynamicWidth = Math.min(
lineWidth[1],
lineWidth[0] + state.mouseSpeed * 0.5,
);
// Update each line
state.lines.forEach((line) => {
const targetWithOffset = {
x: state.target.x + line.offset.x,
y: state.target.y + line.offset.y,
};
// Update points from tail to head
for (let i = line.points.length - 1; i >= 0; i--) {
if (i === 0) {
// Head follows target with spring physics
const forceX =
(targetWithOffset.x - line.points[i].x) * line.spring;
const forceY =
(targetWithOffset.y - line.points[i].y) * line.spring;
line.velocity.x = (line.velocity.x + forceX) * line.friction;
line.velocity.y = (line.velocity.y + forceY) * line.friction;
line.points[i].x += line.velocity.x;
line.points[i].y += line.velocity.y;
} else {
// Rest follow the previous point with lerp
const lerpFactor = 0.85;
line.points[i].x +=
(line.points[i - 1].x - line.points[i].x) * lerpFactor;
line.points[i].y +=
(line.points[i - 1].y - line.points[i].y) * lerpFactor;
}
}
// Draw line
drawLine(line.points, line.color, dynamicWidth);
});
animationRef.current = requestAnimationFrame(animate);
}
// Mouse/touch event handlers
const handleMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
state.target.x = e.clientX - rect.left;
state.target.y = e.clientY - rect.top;
};
const handleTouchMove = (e: TouchEvent) => {
if (e.touches.length > 0) {
const rect = canvas.getBoundingClientRect();
state.target.x = e.touches[0].clientX - rect.left;
state.target.y = e.touches[0].clientY - rect.top;
state.isTouch = true;
}
};
const handleMouseLeave = () => {
// Smoothly return to center when mouse leaves
state.target.x = centerX;
state.target.y = centerY;
};
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("touchstart", handleTouchMove, { passive: true });
canvas.addEventListener("touchmove", handleTouchMove, { passive: true });
canvas.addEventListener("mouseleave", handleMouseLeave);
// Start animation
animate();
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("touchstart", handleTouchMove);
canvas.removeEventListener("touchmove", handleTouchMove);
canvas.removeEventListener("mouseleave", handleMouseLeave);
};
}, [
size,
lineCount,
pointCount,
lineWidth,
colors,
backgroundColor,
spring,
friction,
]);
return (
<div className={`relative ${className}`}>
<canvas
ref={canvasRef}
className="block cursor-none"
style={{ width: size, height: size }}
/>
</div>
);
}
// ============================================================================
// EXPORTS
// ============================================================================
export { TrailingCursor };import TrailingCursor from "@/components/TrailingCursor";
// Basic usage with default red gradient
export default function BasicExample() {
return (
<div className="bg-gray-900 min-h-screen flex items-center justify-center">
<TrailingCursor size={400} />
</div>
);
}
// Blue gradient variation
export function BlueTrail() {
return (
<TrailingCursor
size={400}
colors={["#0066FF", "#00AAFF", "#00DDFF", "#66FFFF"]}
/>
);
}
// More lines with custom physics
export function DenseTrail() {
return (
<TrailingCursor
size={400}
lineCount={6}
lineWidth={[2, 10]}
spring={0.08}
friction={0.9}
/>
);
}
// Fullscreen background effect
export function FullscreenTrail() {
const [dimensions, setDimensions] = useState({
width: typeof window !== "undefined" ? window.innerWidth : 800,
height: typeof window !== "undefined" ? window.innerHeight : 600,
});
useEffect(() => {
const handleResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<div className="fixed inset-0 bg-black">
<TrailingCursor
size={Math.max(dimensions.width, dimensions.height)}
lineCount={6}
colors={["#FF0844", "#FF4B6E", "#8B5CF6", "#0066FF"]}
/>
</div>
);
}