Cursors✨ Premium

Cursor Trail

Ten chained dots that follow the cursor through interpolation: each dot closes 30% of the gap to its predecessor every frame, so the tail lags far behind the head — stretching on sharp turns and retracting at rest.

JSTrail

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →

3 usage examples

Full-screen menu — creative navigation — Cursor Trail example 1

① Full-screen menu — creative navigation

WhenAn overlay menu covering the full viewport, with large typographic links and a custom cursor specified in the brand guidelines.
WhyThe trail physically follows pointer displacement — it tracks, it does not radiate from a fixed point. The wake behind the cursor gives a sense of speed and physicality that reinforces the act of navigating without adding any static element to the page.
Settings5–6 dots for a short, precise tail; lerp at 0.4 to keep it close behind the cursor; white dots at low opacity so they do not compete with the typography.
Ambient hero — dark interactive background — Cursor Trail example 2

② Ambient hero — dark interactive background

WhenA dark hero where the visitor is invited to sweep the mouse across a textured or illustrated background, with no clickable element in the zone.
WhyTen lerping dots produce an organic undulation driven by gesture speed: the tail stretches on turns and retracts at rest. This chain dynamic cannot be replicated with a simple SVG path — each link computes its position independently.
SettingsLerp at 0.12 for a long, floating trail; opacity coefficient at 0.06 so the tail stays visible far back; mix-blend-mode: screen on the dots to blend into the background.
Guided onboarding — trace a path — Cursor Trail example 3

③ Guided onboarding — trace a path

WhenAn onboarding screen or tutorial asking users to sweep the cursor over a specific zone to trigger the next step.
WhyThe trail visualizes the path being traced: immediate visual feedback that confirms the action without an alert or a toast. More convincing than a zone color change because it shows movement, not just state.
Settings15–18 dots for a persistent tail; lerp at 0.2 so the tail stays visible after a direction change; use a validation color distinct from the exploration color to signal activated zones.

How it works

At startup, the script creates ten div elements (class cursor-trail), appends them to document.body, and stores them in trailDots with coordinates initialized to (0, 0). Each dot receives a fixed opacity at creation — 1 − i × 0.1, from 1.0 to 0.1 — and a fixed scale — scale(1 − i × 0.08), from 1.0 to 0.28. These values never change. All dots stay at display: none until mouseenter on the zone; mouseleave hides them. The cursor position is captured into trailX / trailY on every mousemove.

All the kinematics happen inside animateTrail(), driven by requestAnimationFrame at startup and running continuously. Each frame, the ten dots are iterated in order. Dot 0 pursues the current cursor position; each subsequent dot pursues the one before it. The formula is the same for all: dot.x += (prev.x − dot.x) × 0.3. Covering 30% of the remaining gap each frame is an asymptotic interpolation that never overshoots. The lag accumulates link by link: the last dot has only (0.7)^9 ≈ 4% of the head's responsiveness.

Two implementation details have direct consequences. The dots are appended to body, not to the zone: their coordinates (clientX / clientY) are viewport positions — nothing clips them. And the rAF loop never stops: while display: none hides the dots, the ten elements keep interpolating toward the last known coordinates — when mouseenter shows them again, the trail resumes without a jolt. Each dot's position is offset by −5 px on both axes to center a 10 × 10 px dot.

Accessibility

  • prefers-reduced-motion: absent from the code. The rAF loop runs unconditionally and cursor-active hides the native cursor regardless. Gate the initial animateTrail() call and the event listeners behind an inverted window.matchMedia('(prefers-reduced-motion: reduce)') check — users who enabled that preference should not lose their cursor.
  • On touch screens, no mousemove fires and the trail is inert. However, a touchstart can trigger a synthetic mouseenter on some browsers, activating cursor-active and hiding the cursor with no visible trail. Gate the listeners behind matchMedia('(hover: hover) and (pointer: fine)') to block that emulation.
  • Multiple instantiated zones means as many parallel rAF loops. To stop the loop outside hover, store the rAF id, call cancelAnimationFrame() on mouseleave, and restart on mouseenter. For several simultaneous zones, a single shared global loop managing all active instances is the cleaner approach.
  • The ten dots are empty divs in body, carrying no role or text. Add aria-hidden="true" to each dot during the creation loop to explicitly exclude them from the accessibility tree and prevent a screen reader from enumerating them during navigation.

Browser compatibility

The limit is on the JavaScript side: arrow functions and a template literal (for the scale() attribute). IE 11 halts on a syntax error before the first dot is created. The CSS that positions the dots (position: fixed, pointer-events: none) and hides the native cursor (cursor: none) has been universal since 2012.

Chrome 49+✓ Full
Firefox 44+✓ Full
Safari 10+✓ Full
Edge 15+✓ Full
Mobile iOS✓ Inert (touch)
Android Chrome✓ Inert (touch)

Without JavaScript, no dots are created and no trail appears — a sound fallback: the effect carries nothing functional. On mobile, without <code>mousemove</code>, dots stay at <code>display: none</code> and the system cursor shows normally. If you need ES5 coverage, transpile the script: the logic has no other dependency.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Number of dots (10 in the initialization loop) 10 Direct control of trail length. Each extra dot is one more div in the DOM and one more lerp per frame — stay below 20. 4–5 = short, tight tail; 15–18 = an undulation that persists long after a sharp turn.
Interpolation factor (0.3 in animateTrail) 0.3 Fraction of the remaining gap each dot closes per frame. 0.1 = a very floaty trail, the tail lingers; 0.5 = a taut trail, the tail catches the head quickly. Adjust this before touching the dot count.
Opacity gradient (coefficient 0.1 per rank) 1 − i × 0.1 Set at creation, never updated. 0.05 = very gradual fade; 0.15 = abrupt fade. With 10 dots, do not exceed 0.1: beyond that, the later ranks are invisible from the moment of creation.
Size gradient (coefficient 0.08 per rank) scale(1 − i × 0.08) Also frozen at creation. 0.05 gives a tail at 55%; 0.12 gives the last dot at 8% — nearly invisible. Combine with the opacity gradient to tune size and visibility independently.
Centering offset (−5 in the JS) −5 px on both axes Position written as dot.x − 5 and dot.y − 5 to center a 10 × 10 px dot. CSS size at 8 px → offset −4; at 16 px → offset −8. A mismatched offset shifts the entire trail away from the true pointer.
Parent element for dots (document.body) document.body Dots use clientX / clientY — viewport coordinates, correct only when the parent has no positioning context of its own. To clip the trail to the zone, move appendChild to the zone and switch to e.offsetX / e.offsetY with overflow: hidden.

FAQ

With ten dots, the impact is imperceptible: ten lerps and ten inline style writes per frame are negligible. The real risk is accumulation: multiple instantiated zones means as many parallel rAF loops. The clean solution is a single shared global loop. To stop the loop outside hover, store the rAF id, call cancelAnimationFrame() on mouseleave, and restart on mouseenter.
Because their coordinates are clientX / clientY — viewport positions. Appended to body, they place correctly without any offset calculation. Inside a parent with its own positioning context, the origin shifts and all dots drift. To confine them to the zone, switch to e.offsetX / e.offsetY with position: absolute inside a position: relative parent and overflow: hidden.
All presentation goes through the cursor-trail CSS class: change border-radius to go from circle to square; background for a flat color or radial gradient; box-shadow for a glow. If you change the CSS size, adjust the −5 centering offset accordingly. To give a different color to each rank, access trailDots[i].el during the initialization loop and set the color directly on the element.