You Might Also Like
Hover Image Trail — Free HTML CSS JS Cursor Effect Snippet
Hover Image Trail · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Hover Image Trail — Distance-Spaced Cursor Image Trail

The hover image trail is the interactive showpiece on creative portfolios and agency sites: as you move the cursor across a section, a stream of images appears along the path, each popping in with a little rotation and then fading away behind the pointer. This snippet builds the complete effect in plain HTML, CSS, and vanilla JavaScript, with the key detail that makes it feel right — the trail is spaced by distance traveled, not by time.
Distance-based spawning
The naive version drops an image on every mousemove event, which floods the screen when you move slowly and leaves gaps when you move fast. Instead, this snippet tracks the last spawn position and only releases a new image once the pointer has traveled past a threshold of 70 pixels, measured with Math.sqrt(dx*dx + dy*dy). The result is an evenly spaced trail no matter how quickly or slowly the cursor moves — the images mark out the path's geometry rather than its timing.
A pool of recycled images
Rather than creating and destroying DOM nodes constantly, the snippet builds a fixed pool of eight <img> elements once and cycles through them with a modulo index (idx % imgs.length). Each spawn reuses the next image in the pool, repositions it under the pointer, and replays its animation. Reusing a small pool keeps the DOM stable and avoids the garbage-collection churn of endlessly appending and removing elements.
Replaying the CSS animation
Each image's entrance is a single hiPop keyframe that scales it up from 0.4×, snaps any rotation back to straight, then fades out while drifting down and counter-rotating. To make the same element replay its animation on every reuse, the code removes the show class, forces a reflow by reading offsetWidth, then re-adds the class. That void img.offsetWidth line is the classic trick to restart a CSS animation — without it the browser coalesces the class changes and the animation never re-triggers.
Per-image randomized rotation
Each spawn sets a CSS custom property --r to a random angle between -13° and 13°. The keyframe reads --r so every image enters at a slightly different tilt and exits counter-rotating, which keeps the trail organic instead of looking like identical stamps. Because the randomness lives in a CSS variable, the animation itself stays a single shared keyframe.
Dependency-free image tiles
So the snippet runs with zero assets, each "photo" is generated as an inline SVG gradient encoded as a data URI. In a real build you'd point each pool image's src at actual photos — the spawning, spacing, and animation logic doesn't change. The images are pointer-events: none so they never intercept the cursor, and the heading is too, so movement tracking is uninterrupted.
Touch support
A touchmove listener (registered passive) feeds the same onMove function using the first touch point, so dragging a finger across the stage produces the same trail on mobile. The stage sets touch-action: none so the gesture isn't hijacked by the browser's scroll.
Customizing it
Lower the threshold for a denser trail or raise it for a sparser one, change the image dimensions, lengthen the hiPop duration for slower fades, or widen the random rotation range for more chaos. Swap the SVG tiles for your portfolio images and you have a signature landing interaction. Pair it with a sparkles text headline or a custom cursor for a fully bespoke pointer experience.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the spacing math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the trail is spaced using the Pythagorean distance between the last and current pointer position instead of a fixed timer, or what the void img.offsetWidth line is actually forcing the browser to do before the show class is re-added. The same assistant is useful for optimizing it — ask whether eight pooled images is enough for fast, erratic mouse movement or whether the threshold value should scale with pool size to avoid running out of images to reuse. It's just as handy for extending the effect: ask it to vary the image size based on cursor speed, make the trail respond to touch pressure on supporting devices, or swap the SVG gradient tiles for real lazy-loaded photographs with a fallback placeholder. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a cursor-following image trail effect in plain HTML, CSS, and JavaScript — no library, no canvas.
Requirements:
- A full-height stage element that tracks mousemove (and touchmove for mobile) events.
- A fixed pool of image elements created once up front (not created and destroyed on every movement), cycled through with a modulo index so the pool is reused indefinitely without growing the DOM.
- Track the last position at which an image was spawned, and on every pointer move compute the straight-line distance from that last spawn position to the current position using the Pythagorean theorem; only spawn a new image once that distance exceeds a fixed pixel threshold, so the trail is spaced evenly by distance traveled regardless of how fast or slow the cursor moves.
- Each spawned image must be positioned at the pointer's coordinates relative to the stage (not the viewport), assigned a random rotation angle via a CSS custom property, and play a single CSS keyframe animation that scales it up from a small size with the random rotation, snaps to full size and zero rotation, then fades out while drifting downward and rotating the opposite direction.
- Because the same pooled image elements are reused, force the CSS animation to replay from scratch on every reuse: remove the animation-triggering class, force a synchronous layout reflow by reading a layout property like offsetWidth, then re-add the class.
- All images and text must have pointer-events disabled so nothing interferes with pointer tracking, and touch handling must prevent the page from scrolling while dragging across the stage.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA full-height stage renders with a centered prompt.
- 2Move the cursorImages appear along the path, popping in with a slight tilt.
- 3Move fast or slowThe trail stays evenly spaced because it is distance-based.
- 4Watch them fadeEach image drifts down, counter-rotates, and fades out.
- 5Try on touchDrag a finger to produce the same trail on mobile.
- 6Swap in real photosPoint each pool image src at your portfolio images.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Time-based spawning floods the screen when the cursor moves slowly and leaves gaps when it moves fast. By only spawning after the pointer travels past a 70px threshold — measured with the Pythagorean distance between the last and current position — the images mark out the path evenly regardless of cursor speed, which is what makes the trail look deliberate.
It builds a fixed pool of eight img elements once and cycles through them with a modulo index. Each spawn reuses the next image in the pool — repositioning it and replaying its animation — so the DOM size stays constant and there's no garbage-collection churn from constantly appending and removing elements.
The code removes the show class, reads img.offsetWidth to force a synchronous reflow, then re-adds the class. That reflow is the classic trick that makes the browser register the class removal before the re-add, so the CSS keyframe restarts. Without it, the two class changes would be batched and the animation would not replay.
Yes. The demo generates SVG gradient tiles as data URIs only so it has zero dependencies. In production, set each pool image's src to a real photo URL. The spacing, recycling, rotation, and fade logic are independent of the image source, so nothing else changes.
Create the image pool with refs (an array of refs or a single container ref you query) and attach mousemove and touchmove listeners in a mount effect with cleanup. Keep the spawn index and last position in refs, not state, so movement doesn't trigger re-renders. The CSS keyframe and the reflow-based replay work unchanged; in Tailwind, express the keyframe in the config and the rotation via an inline --r style.