webdevpuneet.com

Cursor Particle Brush Trail — Free Canvas Lingering Trail Snippet

Cursor Particle Brush Trail · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<section class="ctp-wrap">
  <span class="ctp-tag">canvas 2d · lingering brush stroke</span>
  <h1>Paint with your cursor</h1>
  <p>Move over the canvas \u2014 particles linger and slowly fade like a brush stroke, instead of vanishing instantly behind the cursor.</p>

  <div class="ctp-stage">
    <canvas class="ctp-canvas" id="ctpCanvas"></canvas>
  </div>

  <button type="button" class="ctp-clear" id="ctpClear">Clear canvas</button>
</section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Lingering particle life
Each particle fades over ~1.5s, not the next frame.
Low-alpha fill, not clearRect
A translucent overlay ages old strokes gradually.
Segment interpolation
Fast swipes still paint a continuous stroke.
Life-driven opacity and radius
Particles fade and shrink together as they age.
Touch and mouse painting
Identical brush behavior on both input types.
Hover-continuous painting
No click required; the brush follows the cursor.
One-tap clear
Resets both the particle list and the canvas pixels.
DPR-aware canvas
Sized for crisp rendering on high-density displays.

About this UI Snippet

Cursor Particle Brush Trail — A Stroke That Lingers, Not a Trail That Vanishes

Screenshot of the Cursor Particle Brush Trail snippet rendered live

Most cursor-trail effects erase the previous frame completely and redraw, so the "trail" only ever spans the last few pixels behind the pointer. This snippet does the opposite on purpose: every particle it spawns has its own life counter and keeps rendering, fading gradually, for about 1.5 seconds — moving the cursor slowly leaves a visible, lingering stroke behind it, closer to a paintbrush than a comet tail.

Persistent particles with individual life, not a cleared-and-redrawn trail

Each particle pushed into the particles array carries its own life/maxLife pair. Every animation frame decrements every particle's life and removes it only once that reaches zero — particles from several frames ago are still being drawn (at lower opacity) alongside particles spawned this frame. That's the entire difference from a typical trail effect, where old points are gone the instant the next frame paints.

A near-transparent fill instead of clearRect

Rather than ctx.clearRect(), every frame paints ctx.fillRect() with a background color at a very low alpha (rgba(5,7,16,0.06)) over the whole canvas. Because that fill only partially obscures what's underneath, particles already on the canvas darken and fade gradually across many frames instead of disappearing in one step — the canvas equivalent of watercolor drying rather than a whiteboard eraser.

Interpolating between mouse positions

A fast swipe can jump many pixels between two consecutive mousemove events, which would otherwise paint sparse, disconnected dots. paintTo() measures the distance from the last painted point and spawns particles at evenly interpolated positions along that segment (roughly every 4px), so even a quick motion leaves a continuous stroke rather than a dashed line.

Opacity and radius both track remaining life

Each particle's alpha is simply life / maxLife, applied to its hsla fill color, and its radius shrinks slightly as life drains (r * (0.5 + alpha * 0.5)) — combining a fade with a subtle shrink is what makes a lingering particle read as dissipating paint rather than a static dot that abruptly blinks out.

Customizing it

Change MAX_LIFE for a shorter or longer-lingering stroke, adjust the background fill's alpha for a faster or slower fade, swap the HUES array for a different palette, or spawn more particles per point for a denser brush. Pair it with canvas audio frequency bars as another real-time canvas rendering reference, or hover image trail for an image-based cursor trail instead.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn empty dark canvas renders with a Clear canvas button.
  2. 2
    Move the cursor over the canvasColorful particles paint along the path and linger.
  3. 3
    Move slowly vs. quicklySlow motion leaves a dense stroke; fast motion a lighter one.
  4. 4
    Wait without movingThe existing stroke keeps fading gradually on its own.
  5. 5
    Click Clear canvasInstantly removes every particle and resets the canvas.
  6. 6
    Tune the lingerChange MAX_LIFE and the background fill's alpha.

Real-world uses

Common Use Cases

Playful landing sections
Reward pointer movement with particles that linger for about 1.5 seconds, like a brush stroke rather than a momentary trail.
Creative coding demos
Provide a canvas particle-system reference, where each particle carries its own life and fades in both opacity and radius as it ages.
Portfolio backgrounds
Pair with a hover image trail for a portfolio that responds to the cursor in two different ways.
Kids' drawing toys
Give a lightweight paint-like canvas to young users, with segment interpolation keeping fast swipes continuous instead of dotted.
Low-alpha fade technique
Learn why a translucent overlay, instead of clearRect, lets older strokes age gradually while new ones draw on top.
Related: FLIP Technique List Reorder Animation
See the FLIP Technique List Reorder Animation for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each particle object carries its own life and maxLife values. Every animation frame decrements every particle's life and only removes it from the array once life reaches zero — meaning particles spawned several frames ago are still drawn, just at progressively lower opacity, alongside particles spawned in the current frame. A typical instantaneous trail instead clears everything from the previous frame before drawing the new one, so nothing survives past a single frame.

clearRect wipes the canvas to fully transparent in one step, which would erase every particle immediately regardless of its own life value. Filling the whole canvas with a background color at a very low alpha each frame only partially obscures whatever was drawn underneath, so previously-painted particles darken and fade across many frames rather than vanishing the instant the next frame renders — this is what produces the "brush stroke drying" look instead of an instant wipe.

paintTo() measures the straight-line distance between the current pointer position and the last painted position, then spawns particles at several evenly-interpolated points along that segment (roughly one every 4px) instead of only at the two endpoints. Without this, a fast mouse movement that jumps many pixels between two mousemove events would leave visible gaps in the stroke.

Each particle's opacity is computed directly from its remaining life (life / maxLife) and applied to its hsla fill color, while its drawn radius also shrinks slightly as life drains. Combining a fade with a subtle shrink — rather than only fading, or only shrinking — is what reads as dissipating paint instead of a static dot abruptly blinking out at the end of its life.

Keep the particles array, the last-point reference, and the animation frame id in refs (not state), since they mutate every frame and should never trigger a re-render. Set up the canvas, resize listener, and requestAnimationFrame loop inside a mount effect, attach the pointer/touch listeners to the canvas ref, and cancel the animation frame and remove listeners in the effect's cleanup function.