Patterns Medium
Cursor Trail
Trail of dots that follow the cursor with decreasing opacity and size. Each point follows the previous with lerp interpolation, creating a smooth trailing effect.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #e4e4e7;
min-height: 100vh;
overflow: hidden;
cursor: none;
}
/* ── Info overlay ── */
.info {
position: fixed;
top: 2rem;
left: 50%;
transform: translateX(-50%);
text-align: center;
z-index: 10;
pointer-events: none;
}
.info-title {
font-size: 1.25rem;
font-weight: 700;
color: #f4f4f5;
}
.info-subtitle {
font-size: 0.8rem;
color: #52525b;
margin-top: 0.25rem;
}
/* ── Controls ── */
.controls {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 1rem;
z-index: 10;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.75rem;
padding: 0.75rem 1.25rem;
pointer-events: all;
cursor: default;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-label {
font-size: 0.65rem;
font-weight: 600;
color: #52525b;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.control-slider {
width: 80px;
-webkit-appearance: none;
appearance: none;
height: 3px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
outline: none;
}
.control-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #a78bfa;
border: 2px solid #6d28d9;
cursor: pointer;
}
.control-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #a78bfa;
border: 2px solid #6d28d9;
cursor: pointer;
}
.color-btn {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.15s, transform 0.15s;
}
.color-btn:hover {
transform: scale(1.15);
}
.color-btn.active {
border-color: #fff;
}
/* ── Trail dots ── */
.trail-dot {
position: fixed;
border-radius: 50%;
pointer-events: none;
will-change: transform;
z-index: 5;
}
/* ── Background grid for visual contrast ── */
.bg-grid {
position: fixed;
inset: 0;
background-image: linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
background-size: 60px 60px;
pointer-events: none;
}(function () {
"use strict";
const container = document.getElementById("trail-container");
const countSlider = document.getElementById("count-slider");
const sizeSlider = document.getElementById("size-slider");
const speedSlider = document.getElementById("speed-slider");
const colorBtns = document.querySelectorAll(".color-btn");
const colorSchemes = {
purple: { base: [167, 139, 250], glow: "rgba(139,92,246,0.4)" },
cyan: { base: [34, 211, 238], glow: "rgba(34,211,238,0.4)" },
rose: { base: [251, 113, 133], glow: "rgba(251,113,133,0.4)" },
green: { base: [74, 222, 128], glow: "rgba(74,222,128,0.4)" },
};
let currentColor = "purple";
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
// Trail state
let dots = [];
let points = [];
function createDots(count) {
// Remove existing
dots.forEach((d) => d.el.remove());
dots = [];
points = [];
for (let i = 0; i < count; i++) {
const el = document.createElement("div");
el.className = "trail-dot";
container.appendChild(el);
dots.push({ el });
points.push({ x: mouseX, y: mouseY });
}
updateDotStyles();
}
function updateDotStyles() {
const baseSize = Number(sizeSlider.value);
const scheme = colorSchemes[currentColor];
const [r, g, b] = scheme.base;
dots.forEach((dot, i) => {
const t = i / dots.length;
const size = baseSize * (1 - t * 0.7);
const opacity = 1 - t * 0.85;
dot.el.style.width = size + "px";
dot.el.style.height = size + "px";
dot.el.style.background = `rgba(${r},${g},${b},${opacity})`;
dot.el.style.boxShadow = `0 0 ${size * 1.5}px rgba(${r},${g},${b},${opacity * 0.5})`;
});
}
// ── Mouse tracking ──
window.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
// ── Animation loop ──
function animate() {
const ease = Number(speedSlider.value) / 100;
// First point follows mouse
if (points.length > 0) {
points[0].x += (mouseX - points[0].x) * ease;
points[0].y += (mouseY - points[0].y) * ease;
}
// Each subsequent point follows the previous
for (let i = 1; i < points.length; i++) {
points[i].x += (points[i - 1].x - points[i].x) * (ease * 0.85);
points[i].y += (points[i - 1].y - points[i].y) * (ease * 0.85);
}
// Apply positions
const baseSize = Number(sizeSlider.value);
for (let i = 0; i < dots.length; i++) {
const t = i / dots.length;
const size = baseSize * (1 - t * 0.7);
dots[i].el.style.transform =
`translate(${points[i].x - size / 2}px, ${points[i].y - size / 2}px)`;
}
requestAnimationFrame(animate);
}
// ── Event listeners ──
countSlider.addEventListener("input", () => {
createDots(Number(countSlider.value));
});
sizeSlider.addEventListener("input", updateDotStyles);
colorBtns.forEach((btn) => {
btn.addEventListener("click", () => {
colorBtns.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
currentColor = btn.dataset.color;
updateDotStyles();
});
});
// ── Init ──
createDots(Number(countSlider.value));
requestAnimationFrame(animate);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cursor Trail</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg-grid"></div>
<div class="info">
<h2 class="info-title">Cursor Trail</h2>
<p class="info-subtitle">Move your mouse around — dots follow with lerp interpolation</p>
</div>
<div class="controls">
<div class="control-group">
<span class="control-label">Count</span>
<input class="control-slider" id="count-slider" type="range" min="5" max="40" value="20" />
</div>
<div class="control-group">
<span class="control-label">Size</span>
<input class="control-slider" id="size-slider" type="range" min="4" max="24" value="12" />
</div>
<div class="control-group">
<span class="control-label">Speed</span>
<input class="control-slider" id="speed-slider" type="range" min="5" max="40" value="15" />
</div>
<div class="control-group">
<span class="control-label">Color</span>
<button class="color-btn active" data-color="purple" style="background:#a78bfa"></button>
<button class="color-btn" data-color="cyan" style="background:#22d3ee"></button>
<button class="color-btn" data-color="rose" style="background:#fb7185"></button>
<button class="color-btn" data-color="green" style="background:#4ade80"></button>
</div>
</div>
<div id="trail-container"></div>
<script src="script.js"></script>
</body>
</html>import { useState, useRef, useEffect, useCallback } from "react";
interface TrailConfig {
count: number;
size: number;
speed: number;
color: string;
}
const colorSchemes: Record<string, { base: [number, number, number]; label: string; hex: string }> =
{
purple: { base: [167, 139, 250], label: "Purple", hex: "#a78bfa" },
cyan: { base: [34, 211, 238], label: "Cyan", hex: "#22d3ee" },
rose: { base: [251, 113, 133], label: "Rose", hex: "#fb7185" },
green: { base: [74, 222, 128], label: "Green", hex: "#4ade80" },
};
export default function CursorTrail() {
const [config, setConfig] = useState<TrailConfig>({
count: 20,
size: 12,
speed: 15,
color: "purple",
});
const canvasRef = useRef<HTMLDivElement>(null);
const dotsRef = useRef<HTMLDivElement[]>([]);
const pointsRef = useRef<{ x: number; y: number }[]>([]);
const mouseRef = useRef({ x: 0, y: 0 });
const configRef = useRef(config);
configRef.current = config;
// Create/recreate dots
useEffect(() => {
if (!canvasRef.current) return;
// Clear old dots
dotsRef.current.forEach((d) => d.remove());
dotsRef.current = [];
pointsRef.current = [];
const { count, size, color } = config;
const [r, g, b] = colorSchemes[color].base;
for (let i = 0; i < count; i++) {
const t = i / count;
const dotSize = size * (1 - t * 0.7);
const opacity = 1 - t * 0.85;
const el = document.createElement("div");
el.style.position = "fixed";
el.style.borderRadius = "50%";
el.style.pointerEvents = "none";
el.style.willChange = "transform";
el.style.zIndex = "5";
el.style.width = dotSize + "px";
el.style.height = dotSize + "px";
el.style.background = `rgba(${r},${g},${b},${opacity})`;
el.style.boxShadow = `0 0 ${dotSize * 1.5}px rgba(${r},${g},${b},${opacity * 0.5})`;
canvasRef.current.appendChild(el);
dotsRef.current.push(el);
pointsRef.current.push({ x: mouseRef.current.x, y: mouseRef.current.y });
}
}, [config.count, config.size, config.color]);
// Update styles when size/color changes without recreating
const updateStyles = useCallback(() => {
const { size, color } = configRef.current;
const [r, g, b] = colorSchemes[color].base;
const count = dotsRef.current.length;
dotsRef.current.forEach((el, i) => {
const t = i / count;
const dotSize = size * (1 - t * 0.7);
const opacity = 1 - t * 0.85;
el.style.width = dotSize + "px";
el.style.height = dotSize + "px";
el.style.background = `rgba(${r},${g},${b},${opacity})`;
el.style.boxShadow = `0 0 ${dotSize * 1.5}px rgba(${r},${g},${b},${opacity * 0.5})`;
});
}, []);
// Animation loop
useEffect(() => {
let raf: number;
function animate() {
const { speed, size } = configRef.current;
const ease = speed / 100;
const pts = pointsRef.current;
const dots = dotsRef.current;
const mouse = mouseRef.current;
if (pts.length > 0) {
pts[0].x += (mouse.x - pts[0].x) * ease;
pts[0].y += (mouse.y - pts[0].y) * ease;
}
for (let i = 1; i < pts.length; i++) {
pts[i].x += (pts[i - 1].x - pts[i].x) * (ease * 0.85);
pts[i].y += (pts[i - 1].y - pts[i].y) * (ease * 0.85);
}
for (let i = 0; i < dots.length; i++) {
const t = i / dots.length;
const dotSize = size * (1 - t * 0.7);
dots[i].style.transform =
`translate(${pts[i].x - dotSize / 2}px, ${pts[i].y - dotSize / 2}px)`;
}
raf = requestAnimationFrame(animate);
}
raf = requestAnimationFrame(animate);
return () => cancelAnimationFrame(raf);
}, []);
// Mouse tracking
useEffect(() => {
const onMove = (e: MouseEvent) => {
mouseRef.current.x = e.clientX;
mouseRef.current.y = e.clientY;
};
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
const sliders: { key: "count" | "size" | "speed"; label: string; min: number; max: number }[] = [
{ key: "count", label: "Count", min: 5, max: 40 },
{ key: "size", label: "Size", min: 4, max: 24 },
{ key: "speed", label: "Speed", min: 5, max: 40 },
];
return (
<div style={{ minHeight: "100vh", background: "#0a0a0a", overflow: "hidden", cursor: "none" }}>
{/* Background grid */}
<div
style={{
position: "fixed",
inset: 0,
pointerEvents: "none",
backgroundImage:
"linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)",
backgroundSize: "60px 60px",
}}
/>
{/* Info */}
<div
style={{
position: "fixed",
top: "2rem",
left: "50%",
transform: "translateX(-50%)",
textAlign: "center",
zIndex: 10,
pointerEvents: "none",
}}
>
<h2
style={{
fontSize: "1.25rem",
fontWeight: 700,
color: "#f4f4f5",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Cursor Trail
</h2>
<p
style={{
fontSize: "0.8rem",
color: "#52525b",
marginTop: "0.25rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Move your mouse around — dots follow with lerp interpolation
</p>
</div>
{/* Controls */}
<div
style={{
position: "fixed",
bottom: "2rem",
left: "50%",
transform: "translateX(-50%)",
display: "flex",
gap: "1rem",
zIndex: 10,
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: "0.75rem",
padding: "0.75rem 1.25rem",
cursor: "default",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
{sliders.map(({ key, label, min, max }) => (
<div key={key} style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span
style={{
fontSize: "0.65rem",
fontWeight: 600,
color: "#52525b",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
{label}
</span>
<input
type="range"
min={min}
max={max}
value={config[key]}
onChange={(e) => setConfig((c) => ({ ...c, [key]: Number(e.target.value) }))}
style={{
width: 80,
WebkitAppearance: "none",
appearance: "none" as never,
height: 3,
background: "rgba(255,255,255,0.1)",
borderRadius: 2,
outline: "none",
}}
/>
</div>
))}
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span
style={{
fontSize: "0.65rem",
fontWeight: 600,
color: "#52525b",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
Color
</span>
{Object.entries(colorSchemes).map(([key, { hex }]) => (
<button
key={key}
onClick={() => setConfig((c) => ({ ...c, color: key }))}
style={{
width: 20,
height: 20,
borderRadius: "50%",
background: hex,
border: config.color === key ? "2px solid #fff" : "2px solid transparent",
cursor: "pointer",
transition: "border-color 0.15s, transform 0.15s",
}}
/>
))}
</div>
</div>
{/* Trail container */}
<div ref={canvasRef} />
</div>
);
}<script setup>
import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
const colorSchemes = {
purple: { base: [167, 139, 250], label: "Purple", hex: "#a78bfa" },
cyan: { base: [34, 211, 238], label: "Cyan", hex: "#22d3ee" },
rose: { base: [251, 113, 133], label: "Rose", hex: "#fb7185" },
green: { base: [74, 222, 128], label: "Green", hex: "#4ade80" },
};
const config = reactive({ count: 20, size: 12, speed: 15, color: "purple" });
const canvasEl = ref(null);
let dots = [];
let points = [];
const mouse = { x: 0, y: 0 };
let raf;
const sliders = [
{ key: "count", label: "Count", min: 5, max: 40 },
{ key: "size", label: "Size", min: 4, max: 24 },
{ key: "speed", label: "Speed", min: 5, max: 40 },
];
function createDots() {
if (!canvasEl.value) return;
dots.forEach((d) => d.remove());
dots = [];
points = [];
const { count, size, color } = config;
const [r, g, b] = colorSchemes[color].base;
for (let i = 0; i < count; i++) {
const t = i / count;
const dotSize = size * (1 - t * 0.7);
const opacity = 1 - t * 0.85;
const el = document.createElement("div");
el.style.position = "fixed";
el.style.borderRadius = "50%";
el.style.pointerEvents = "none";
el.style.willChange = "transform";
el.style.zIndex = "5";
el.style.width = dotSize + "px";
el.style.height = dotSize + "px";
el.style.background = `rgba(${r},${g},${b},${opacity})`;
el.style.boxShadow = `0 0 ${dotSize * 1.5}px rgba(${r},${g},${b},${opacity * 0.5})`;
canvasEl.value.appendChild(el);
dots.push(el);
points.push({ x: mouse.x, y: mouse.y });
}
}
function animate() {
const { speed, size } = config;
const ease = speed / 100;
if (points.length > 0) {
points[0].x += (mouse.x - points[0].x) * ease;
points[0].y += (mouse.y - points[0].y) * ease;
}
for (let i = 1; i < points.length; i++) {
points[i].x += (points[i - 1].x - points[i].x) * (ease * 0.85);
points[i].y += (points[i - 1].y - points[i].y) * (ease * 0.85);
}
for (let i = 0; i < dots.length; i++) {
const t = i / dots.length;
const dotSize = size * (1 - t * 0.7);
dots[i].style.transform =
`translate(${points[i].x - dotSize / 2}px, ${points[i].y - dotSize / 2}px)`;
}
raf = requestAnimationFrame(animate);
}
function onMouseMove(e) {
mouse.x = e.clientX;
mouse.y = e.clientY;
}
function updateSlider(key, e) {
config[key] = Number(e.target.value);
}
function setColor(key) {
config.color = key;
}
watch(
() => [config.count, config.size, config.color],
() => createDots()
);
onMounted(() => {
createDots();
raf = requestAnimationFrame(animate);
window.addEventListener("mousemove", onMouseMove);
});
onUnmounted(() => {
cancelAnimationFrame(raf);
window.removeEventListener("mousemove", onMouseMove);
dots.forEach((d) => d.remove());
});
</script>
<template>
<div class="cursor-trail-root">
<div class="bg-grid"></div>
<div class="info">
<h2>Cursor Trail</h2>
<p>Move your mouse around — dots follow with lerp interpolation</p>
</div>
<div class="controls">
<div v-for="s in sliders" :key="s.key" class="slider-group">
<span class="slider-label">{{ s.label }}</span>
<input type="range" :min="s.min" :max="s.max" :value="config[s.key]"
@input="updateSlider(s.key, $event)" />
</div>
<div class="slider-group">
<span class="slider-label">Color</span>
<button v-for="(scheme, key) in colorSchemes" :key="key" class="color-btn"
:style="{ background: scheme.hex, border: `2px solid ${config.color === key ? '#fff' : 'transparent'}` }"
@click="setColor(key)"></button>
</div>
</div>
<div ref="canvasEl"></div>
</div>
</template>
<style scoped>
.cursor-trail-root { min-height: 100vh; background: #0a0a0a; overflow: hidden; cursor: none; }
.bg-grid { position: fixed; inset: 0; pointer-events: none; background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px); background-size: 60px 60px; }
.info { position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); text-align: center; z-index: 10; pointer-events: none; }
.info h2 { font-size: 1.25rem; font-weight: 700; color: #f4f4f5; font-family: system-ui, -apple-system, sans-serif; margin: 0; }
.info p { font-size: 0.8rem; color: #52525b; margin-top: 0.25rem; font-family: system-ui, -apple-system, sans-serif; }
.controls { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); display: flex; gap: 1rem; z-index: 10; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.75rem; padding: 0.75rem 1.25rem; cursor: default; font-family: system-ui, -apple-system, sans-serif; }
.slider-group { display: flex; align-items: center; gap: 0.5rem; }
.slider-label { font-size: 0.65rem; font-weight: 600; color: #52525b; text-transform: uppercase; letter-spacing: 0.06em; }
input[type="range"] { width: 80px; -webkit-appearance: none; appearance: none; height: 3px; background: rgba(255,255,255,0.1); border-radius: 2px; outline: none; }
.color-btn { width: 20px; height: 20px; border-radius: 50%; cursor: pointer; transition: border-color 0.15s, transform 0.15s; padding: 0; }
</style><script>
import { onMount, onDestroy } from "svelte";
const colorSchemes = {
purple: { base: [167, 139, 250], label: "Purple", hex: "#a78bfa" },
cyan: { base: [34, 211, 238], label: "Cyan", hex: "#22d3ee" },
rose: { base: [251, 113, 133], label: "Rose", hex: "#fb7185" },
green: { base: [74, 222, 128], label: "Green", hex: "#4ade80" },
};
let config = { count: 20, size: 12, speed: 15, color: "purple" };
let canvasEl;
let dots = [];
let points = [];
let mouse = { x: 0, y: 0 };
let raf;
const sliders = [
{ key: "count", label: "Count", min: 5, max: 40 },
{ key: "size", label: "Size", min: 4, max: 24 },
{ key: "speed", label: "Speed", min: 5, max: 40 },
];
function createDots() {
if (!canvasEl) return;
dots.forEach((d) => d.remove());
dots = [];
points = [];
const { count, size, color } = config;
const [r, g, b] = colorSchemes[color].base;
for (let i = 0; i < count; i++) {
const t = i / count;
const dotSize = size * (1 - t * 0.7);
const opacity = 1 - t * 0.85;
const el = document.createElement("div");
el.style.position = "fixed";
el.style.borderRadius = "50%";
el.style.pointerEvents = "none";
el.style.willChange = "transform";
el.style.zIndex = "5";
el.style.width = dotSize + "px";
el.style.height = dotSize + "px";
el.style.background = `rgba(${r},${g},${b},${opacity})`;
el.style.boxShadow = `0 0 ${dotSize * 1.5}px rgba(${r},${g},${b},${opacity * 0.5})`;
canvasEl.appendChild(el);
dots.push(el);
points.push({ x: mouse.x, y: mouse.y });
}
}
function animate() {
const { speed, size } = config;
const ease = speed / 100;
if (points.length > 0) {
points[0].x += (mouse.x - points[0].x) * ease;
points[0].y += (mouse.y - points[0].y) * ease;
}
for (let i = 1; i < points.length; i++) {
points[i].x += (points[i - 1].x - points[i].x) * (ease * 0.85);
points[i].y += (points[i - 1].y - points[i].y) * (ease * 0.85);
}
for (let i = 0; i < dots.length; i++) {
const t = i / dots.length;
const dotSize = size * (1 - t * 0.7);
dots[i].style.transform =
`translate(${points[i].x - dotSize / 2}px, ${points[i].y - dotSize / 2}px)`;
}
raf = requestAnimationFrame(animate);
}
function onMouseMove(e) {
mouse.x = e.clientX;
mouse.y = e.clientY;
}
function updateSlider(key, value) {
config = { ...config, [key]: Number(value) };
createDots();
}
function setColor(key) {
config = { ...config, color: key };
createDots();
}
onMount(() => {
createDots();
raf = requestAnimationFrame(animate);
window.addEventListener("mousemove", onMouseMove);
});
onDestroy(() => {
cancelAnimationFrame(raf);
window.removeEventListener("mousemove", onMouseMove);
dots.forEach((d) => d.remove());
});
</script>
<div class="cursor-trail-root" on:mousemove={onMouseMove}>
<div class="bg-grid"></div>
<div class="info">
<h2>Cursor Trail</h2>
<p>Move your mouse around — dots follow with lerp interpolation</p>
</div>
<div class="controls">
{#each sliders as { key, label, min, max }}
<div class="slider-group">
<span class="slider-label">{label}</span>
<input type="range" {min} {max} value={config[key]}
on:input={(e) => updateSlider(key, e.target.value)} />
</div>
{/each}
<div class="slider-group">
<span class="slider-label">Color</span>
{#each Object.entries(colorSchemes) as [key, { hex }]}
<button class="color-btn"
style="background: {hex}; border: 2px solid {config.color === key ? '#fff' : 'transparent'};"
on:click={() => setColor(key)}></button>
{/each}
</div>
</div>
<div bind:this={canvasEl}></div>
</div>
<style>
.cursor-trail-root { min-height: 100vh; background: #0a0a0a; overflow: hidden; cursor: none; }
.bg-grid { position: fixed; inset: 0; pointer-events: none; background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px); background-size: 60px 60px; }
.info { position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); text-align: center; z-index: 10; pointer-events: none; }
.info h2 { font-size: 1.25rem; font-weight: 700; color: #f4f4f5; font-family: system-ui, -apple-system, sans-serif; margin: 0; }
.info p { font-size: 0.8rem; color: #52525b; margin-top: 0.25rem; font-family: system-ui, -apple-system, sans-serif; }
.controls { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); display: flex; gap: 1rem; z-index: 10; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.75rem; padding: 0.75rem 1.25rem; cursor: default; font-family: system-ui, -apple-system, sans-serif; }
.slider-group { display: flex; align-items: center; gap: 0.5rem; }
.slider-label { font-size: 0.65rem; font-weight: 600; color: #52525b; text-transform: uppercase; letter-spacing: 0.06em; }
input[type="range"] { width: 80px; -webkit-appearance: none; appearance: none; height: 3px; background: rgba(255,255,255,0.1); border-radius: 2px; outline: none; }
.color-btn { width: 20px; height: 20px; border-radius: 50%; cursor: pointer; transition: border-color 0.15s, transform 0.15s; padding: 0; }
</style>Cursor Trail
A mesmerizing trail of dots that chase the cursor across the screen. Each dot follows the one ahead of it using linear interpolation (lerp), producing a smooth, organic-feeling trail.
How it works
- An array of trail points is created, each represented by a small
div - On
mousemove, the first point targets the cursor position - Every subsequent point lerps toward the point ahead of it:
pos += (target - pos) * ease - A
requestAnimationFrameloop updates all positions each frame - Opacity and size decrease along the trail for a fading-tail effect
Customization
- Count — number of trail dots
- Size — base diameter of dots
- Color — accent color with gradient falloff
- Speed — lerp factor (lower = more lag, higher = tighter follow)
Use cases
- Creative portfolio cursor effects
- Interactive landing pages
- Game UI cursor feedback