🚀

Update available

We just released a new resource or update, refresh the Vault to access the latest version.

Cancel

Refresh now

Harri

Profile Picture

Harri

Lemke

Draw Path Cursor Effect

Documentation

Webflow

Code

Setup: External Scripts

HTML

Copy
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>

Step 1: Add HTML

HTML

Copy
<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

Copy
.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

Copy
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';
Copy

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;
Copy

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;
Copy

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;
Copy

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;
Copy

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.

Live preview

Osmo Robot AI

Copy context for AI

Beta

Webflow

HTML/CSS/JS

Save video

Copy share link

Resource details

  • Published

    March 4, 2026

  • Category

    Cursor Animations

  • Popularity

    488 visitors

  • Need help?

    Join Slack

2D
Canvas
Cursor
Follow
GSAP
SVG
Effect
Inertia
Momentum
Lerp
Ilja van EckIlja van Eck

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.

s