Cursor effect
Cursor trail effect: a fading line that follows the mouse
A soft line follows the pointer and fades within half a second. It draws on a single full-screen canvas, so it stays smooth and never gets in the way of a click.
Usage
When to use it
- Playful launch pages, event microsites and creative portfolios.
- Hero sections where a little motion invites people to move the mouse.
- Showcases of a drawing or design tool, as a hint of what the product does.
- Use it on one page or one section. On a site people read for a while, the trail gets tiring.
Code
Copy the effect
<canvas class="cursor-trail" aria-hidden="true"></canvas> .cursor-trail {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 9998;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce), (hover: none), (pointer: coarse) {
.cursor-trail { display: none; }
} (() => {
const canvas = document.querySelector('.cursor-trail');
const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)');
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (!canvas || !finePointer.matches) return;
const ctx = canvas.getContext('2d');
const COLOR = '45, 91, 255'; // red, green, blue of the trail
const LIFE = 450; // milliseconds each point stays visible
const MAX_POINTS = 60;
let points = [];
let frame = 0;
function resize() {
const ratio = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(window.innerWidth * ratio);
canvas.height = Math.round(window.innerHeight * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function clear() {
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
}
function draw() {
const now = performance.now();
points = points.filter((point) => now - point.time < LIFE);
clear();
ctx.lineCap = 'round';
for (let i = 1; i < points.length; i++) {
const from = points[i - 1];
const to = points[i];
const life = 1 - (now - to.time) / LIFE;
ctx.strokeStyle = `rgba(${COLOR}, ${(life * 0.8).toFixed(3)})`;
ctx.lineWidth = 1 + life * 7;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
}
frame = points.length > 1 ? requestAnimationFrame(draw) : 0;
}
window.addEventListener('pointermove', (event) => {
if (event.pointerType !== 'mouse' || reduceMotion.matches) return;
points.push({ x: event.clientX, y: event.clientY, time: performance.now() });
if (points.length > MAX_POINTS) points.shift();
if (!frame) frame = requestAnimationFrame(draw);
}, { passive: true });
reduceMotion.addEventListener('change', () => {
if (reduceMotion.matches) {
points = [];
clear();
}
});
window.addEventListener('resize', resize);
resize();
})(); Accessibility
Motion, touch and screen readers
Reduced motion
Two layers. The CSS hides the canvas entirely when prefers-reduced-motion: reduce is set, and the script ignores pointer moves while the setting is on, so nothing is drawn or animated. If someone switches the setting on while the page is open, the script clears the trail at once.
Accessibility notes
- The canvas has aria-hidden="true" and pointer-events: none, so it is invisible to screen readers and never intercepts clicks or text selection.
- The native cursor stays visible. The trail is decoration behind it, not a replacement.
- It only runs with a mouse or trackpad and is hidden on touch screens.
- Fast, high-contrast trails can be uncomfortable for people with vestibular or attention issues. Keep it short-lived and subtle, as the defaults are.
Site builders
Webflow, Framer and React
Webflow
In Webflow, put the CSS inside a <style> tag and the JavaScript inside a <script> tag in the page's custom code (Before </body> tag), or site-wide in Site settings > Custom code. Add the canvas with an Embed element anywhere on the page; it is fixed to the viewport, so its position in the layout does not matter.
Framer
Paste the CSS and JS into Site settings > General > Custom Code (end of <body>) and add the canvas in an Embed. The script listens on window, so it keeps working when Framer changes pages without a reload.
React
Render the canvas once in your root layout and wrap the script in a useEffect. In the cleanup, remove the pointermove, resize and change listeners and call cancelAnimationFrame(frame). Keep points in a local variable inside the effect, not in state, so the trail does not re-render React.
FAQ
Questions
- Will the trail slow my page down?
- It draws only while the mouse moves and stops as soon as the trail fades. On a typical laptop it uses very little time per frame.
- How do I change the colour or length?
- Edit COLOR (three numbers, red, green and blue) and LIFE (milliseconds). A higher LIFE gives a longer tail.
- Can the trail use images or emoji instead of a line?
- Yes. Replace the stroke calls in draw() with ctx.drawImage or ctx.fillText at each point, and fade with ctx.globalAlpha.