Mouse Trail — layered rainbow SVG ribbon that snakes after the cursor
Goal
Build a full-viewport, playful mouse-trail effect. Seven stacked SVG polyline paths in a rainbow palette chase the cursor across a bright amber background. They read as one thick, glossy ribbon that whips and snakes behind the pointer: each colored layer has a slightly different length, so the tails fan out and reveal the colors underneath as the ribbon curls. The star effect is the eased, interpolated lag of the trail as it catches up to a fast-moving cursor.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) — core only. No GSAP plugins, no ScrollTrigger, no lenis, no canvas, no WebGL. The trail geometry is drawn by hand into SVG <path> elements from inside a requestAnimationFrame loop; gsap is used only to ease a single shared pointer object toward the mouse.
Layout / HTML
<body> contains seven sibling <svg> elements, each holding exactly one <path class="trail">. There is no wrapper, no viewBox, no width/height attributes on the SVGs (they are sized to 100% via CSS). Each path starts with an empty d="" and carries its stroke color as an inline style. Order matters — later SVGs paint on top. From first (bottom layer) to last (top layer), the inline stroke colors are:
#fefa02(yellow)#9aa8e1(periwinkle)#f57faa(pink)#fb2832(red-orange)#559ce2(blue)#e27b63(terracotta)#e82c31(crimson)
<svg><path d="" class="trail" style="stroke: #fefa02"></path></svg>
<svg><path d="" class="trail" style="stroke: #9aa8e1"></path></svg>
<svg><path d="" class="trail" style="stroke: #f57faa"></path></svg>
<svg><path d="" class="trail" style="stroke: #fb2832"></path></svg>
<svg><path d="" class="trail" style="stroke: #559ce2"></path></svg>
<svg><path d="" class="trail" style="stroke: #e27b63"></path></svg>
<svg><path d="" class="trail" style="stroke: #e82c31"></path></svg>
<script type="module" src="./script.js"></script>
Styling
- Global reset:
* { margin:0; padding:0; box-sizing:border-box; }. body { background: #ffb503; }— a saturated amber/marigold. No scrollbars needed;
the SVGs are position: absolute so the page does not grow.
- Every
svg: `position:absolute; top:0; left:0; width:100%; height:100%;
pointer-events:none;`. All seven overlap perfectly, stacked in DOM order.
- Every
path: `fill:none; stroke-width:50; stroke-linecap:round;
stroke-linejoin:round;. The fat 50px stroke plus round caps/joins is what turns a jagged polyline into a smooth, rounded ribbon. Stroke color comes from the inline style` on each path (do not set stroke in CSS).
GSAP effect (this is the important part — be exact)
The one shared, eased pointer
There is a single plain object smoothPointer = { x, y } initialized to the viewport center: x = window.innerWidth / 2, y = window.innerHeight / 2. All seven paths read from this same object — there is only one eased pointer for the whole system.
On window mousemove, tween that object toward the raw cursor:
window.addEventListener("mousemove", (event) => {
gsap.to(smoothPointer, {
x: event.clientX,
y: event.clientY,
duration: 0.5,
ease: "power2.out",
});
});
- Animated properties:
x,yofsmoothPointer→ the mouse'sclientX/clientY. duration: 0.5,ease: "power2.out", no delay, no stagger.- Each new mousemove fires a fresh
gsap.to; GSAP overwrites the in-flight tween, so
the pointer continuously eases toward the latest cursor position. This 0.5s eased lag is the source of the whip/snake feel — the trail never snaps instantly.
Per-path point history (defines each tail's length)
Keep an array of maximum point counts, one per path, descending:
const totalPointsArray = [40, 35, 30, 25, 20, 15, 10];
The bottom layer (#fefa02) keeps 40 points → longest tail; the top layer (#e82c31) keeps 10 → shortest tail. Because shorter tails are painted last, they sit on top, so as the ribbon curls you see the longer under-layers fan out beyond the shorter over-layers, exposing the rainbow.
The requestAnimationFrame draw loop
Run a continuous requestAnimationFrame(updatePath) loop (started once, self- recursing). It never stops. Every frame, for each path (via forEach with index):
- Read the path's own history array (store it as a custom property on the element,
e.g. path.points, defaulting to []).
points.unshift({ ...smoothPointer })— prepend a copy of the current shared
pointer to the FRONT of the array (index 0 = newest / cursor end).
- Trim from the tail:
while (points.length > totalPointsArray[index]) points.pop();
so each array holds at most its configured number of the most-recent pointer positions.
- Save it back (
path.points = points). - If
points.length > 1, rebuild the SVG path data as a straight polyline through
the history and path.setAttribute("d", d): ``js let d = M ${points[0].x} ${points[0].y}; for (let i = 1; i < points.length; i++) d += L ${points[i].x} ${points[i].y}; ` Just M (moveTo) then L` (lineTo) segments — no curves, no smoothing. The 50px round stroke does all the visual smoothing.
Because every path samples the same eased pointer each frame but retains a different number of past samples, the seven layers trace the identical route and differ only in how far back in time their tails reach — producing the nested, staggered, multi-color ribbon. When the cursor is still, all points collapse onto one spot and the ribbon shrinks to a stack of round dots; when it moves fast, the tails stretch out into long colored streaks.
Assets / images
None. Pure vector + color; no external images, icons, fonts, or textures.
Behavior notes
- Desktop / pointer-driven only. The effect is entirely
mousemove-based; on
touch devices with no cursor there is nothing to drive it (it simply rests at center). No hover, click, or scroll triggers.
- Initial state: pointer at center, all
dempty, so on load the screen is just the
amber background until the first mouse movement seeds the histories.
- The rAF loop runs forever (no reduced-motion guard in the original). If you add one,
gate the loop and the mousemove tween behind matchMedia("(prefers-reduced-motion: reduce)").
- Everything is fixed to the viewport; the page does not scroll and the SVGs always
fill 100% × 100% of the window.