Draw Path Cursor Effect
Documentation
Webflow
Code
Setup: External Scripts
HTML
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>Step 1: Add HTML
HTML
<div class="cursor-wrap">
<div data-cursor-dot class="cursor-dot"></div>
<canvas data-cursor-canvas class="cursor-canvas"></canvas>
</div>Step 2: Add CSS
CSS
.cursor-wrap {
position: fixed;
inset: 0;
z-index: 1000;
pointer-events: none;
width: 100%;
height: 100%;
}
.cursor-dot {
width: 2em;
height: 2em;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3);
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
.cursor-canvas {
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
position: absolute;
}
@media (max-width: 991px), (prefers-reduced-motion: reduce) {
.cursor-wrap {
display: none;
}
}Step 2: Add Javascript
Step 3: Add Javascript
Javascript
function initDrawPathCursorEffect() {
if (window.matchMedia('(pointer: coarse)').matches) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// Config
const trailDuration = 1250; // how long the trail lingers in ms
const trailColor = '#A1FF62'; // hex color of the trail
const strokeMinWidth = 2; // thinnest line (fast movement)
const strokeMaxWidth = 16; // thickest line (slow movement)
const strokeSmoothing = 0.1; // 0–1 — lower = smoother width transitions
const velocitySlow = 0.08; // px/ms threshold for "slow"
const velocityFast = 2.8; // px/ms threshold for "fast"
const glowBlur = 10; // px — glow radius
const glowIntensity = 0.25; // 0–1 — glow opacity
const cursorLag = 0.15; // seconds — GSAP easing duration
const dot = document.querySelector('[data-cursor-dot]');
const canvas = document.querySelector('[data-cursor-canvas]');
const ctx = canvas.getContext('2d');
let points = [];
let hasMouse = false;
let runningWidth = strokeMinWidth;
function hexToRgb(hex) {
const m = hex.replace('#', '').match(/.{2}/g);
return m.map(c => parseInt(c, 16));
}
const color = hexToRgb(trailColor);
gsap.set(dot, { xPercent: -50, yPercent: -50, opacity: 0 });
const xTo = gsap.quickTo(dot, 'x', { duration: cursorLag, ease: 'power3' });
const yTo = gsap.quickTo(dot, 'y', { duration: cursorLag, ease: 'power3' });
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
ctx.scale(dpr, dpr);
}
resize();
window.addEventListener('resize', resize);
document.addEventListener('mouseenter', () => {
dot.style.opacity = '1';
});
document.addEventListener('mouseleave', () => {
dot.style.opacity = '0';
});
window.addEventListener('mousemove', (e) => {
hasMouse = true;
xTo(e.clientX);
yTo(e.clientY);
});
gsap.ticker.add(() => {
if (!hasMouse) return;
const x = gsap.getProperty(dot, 'x');
const y = gsap.getProperty(dot, 'y');
if (points.length > 0) {
const last = points[points.length - 1];
const dx = x - last.x;
const dy = y - last.y;
if (dx * dx + dy * dy < 0.1) return;
}
points.push({ x, y, time: performance.now() });
});
function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }
function remap(v, inMin, inMax, outMin, outMax) {
const t = clamp((v - inMin) / (inMax - inMin), 0, 1);
return outMin + t * (outMax - outMin);
}
function render() {
const now = performance.now();
ctx.clearRect(0, 0, canvas.width, canvas.height);
points = points.filter(p => now - p.time < trailDuration);
if (points.length >= 3) drawTrail(now);
requestAnimationFrame(render);
}
function drawTrail(now) {
const [r, g, b] = color;
ctx.lineCap = 'butt';
ctx.shadowColor = `rgba(${r}, ${g}, ${b}, ${glowIntensity})`;
ctx.shadowBlur = glowBlur;
for (let i = 1; i < points.length - 1; i++) {
const prev = points[i - 1];
const curr = points[i];
const next = points[i + 1];
const mx1 = (prev.x + curr.x) * 0.5;
const my1 = (prev.y + curr.y) * 0.5;
const mx2 = (curr.x + next.x) * 0.5;
const my2 = (curr.y + next.y) * 0.5;
const dx = curr.x - prev.x;
const dy = curr.y - prev.y;
const dt = curr.time - prev.time || 1;
const velocity = Math.sqrt(dx * dx + dy * dy) / dt;
const targetWidth = remap(velocity, velocitySlow, velocityFast, strokeMaxWidth, strokeMinWidth);
runningWidth += (targetWidth - runningWidth) * strokeSmoothing;
const age = now - curr.time;
const life = 1 - age / trailDuration;
const alpha = life * life;
if (alpha <= 0.005) continue;
ctx.beginPath();
ctx.moveTo(mx1, my1);
ctx.quadraticCurveTo(curr.x, curr.y, mx2, my2);
ctx.lineWidth = runningWidth;
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
ctx.stroke();
}
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
}
requestAnimationFrame(render);
}
// Initialize Draw Path Cursor Effect
document.addEventListener('DOMContentLoaded', () => {
initDrawPathCursorEffect();
});Implementation
This effect adds a smooth trailing line behind a dot that follows the mouse. The dot is animated with GSAP for a soft, lagging feel, and a full-window canvas draws a velocity-sensitive trail that fades over time. Move fast and the line gets thin, move slow and it gets thick. The effect is disabled automatically on touch devices, small screens, and when the user prefers reduced motion.
Trail
Trail duration and color are set at the top of the script. trailDuration controls how long each point in the trail stays alive before fading out, and trailColor sets the hex color for both the line and its glow.
const trailDuration = 1250;
const trailColor = '#A1FF62';Stroke
Stroke width reacts to how fast the cursor moves. strokeMinWidth is the thinnest the line gets at full speed, strokeMaxWidth is the thickest at rest, and strokeSmoothing controls how gradually the width transitions between the two. Lower smoothing values give a more fluid feel.
const strokeMinWidth = 1.5;
const strokeMaxWidth = 16;
const strokeSmoothing = 0.01;Velocity
The velocity thresholds define what counts as slow and fast movement in pixels per millisecond. Everything between velocitySlow and velocityFast is mapped linearly to the stroke width range.
const velocitySlow = 0.08;
const velocityFast = 2.8;Glow
The trail has a soft glow rendered with canvas shadow. glowBlur sets the radius in pixels and glowIntensity controls the opacity of the glow color.
const glowBlur = 10;
onst glowIntensity = 0.25;Cursor lag
Use cursorLag to control how quickly the dot catches up to the actual mouse position. This is the GSAP easing duration in seconds. The trail originates from the dot's animated position, not the raw mouse, so both always stay connected.
const cursorLag = 0.15;Window bounds
The dot hides when the cursor leaves the browser window and reappears when it re-enters. The trail is not cleared on exit, so any existing line fades out naturally.
Resource details
Published
March 4, 2026
Category
Cursor Animations
Popularity
488 visitors
Need help?
Join Slack