All components

Burocratik Image Trail

JavaScript animation component · Published 2026-07-21 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

A cursor-driven image trail: while the mouse moves across the central full-viewport panel, a new 200px photo spawns at the cursor once it passes a distance threshold, scaling up from 0 with a random rotation via a CSS transform transition, then collapsing back to scale 0 and unmounting after a ~750ms lifespan. Idle cursors and scroll events also seed images. Pure vanilla JS driven by requestAnimationFrame and CSS cubic-bezier transitions (no GSAP); Lenis provides smooth scrolling.

How it's built

Categoryinteractive
Techlenis
Complexitysection
Performance costmedium
Mobile-safedesktop-first
Scrollhijacks scrolling

image-trail cursor mouse-trail lenis vanilla-js css-transitions editorial hover scroll

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Cursor Image Trail (vanilla JS + CSS transitions)

Goal

Build a three-section page whose middle, light-colored full-viewport panel spawns a trail of photos under the cursor: every time the mouse travels far enough (or sits idle, or the page scrolls) a new 200×200 image pops in at the cursor position with a random tilt, scaling up from 0, then collapses back to scale 0 and unmounts after a short lifespan. The result is a continuous, self-cleaning stream of tilted photos chasing the pointer.

Tech

Vanilla HTML/CSS/JS with ES module imports. No GSAP is used — the animation engine is a requestAnimationFrame loop plus CSS transform transitions with custom cubic-bezier easings. Install and import lenis (npm) for smooth scrolling. Everything runs inside a DOMContentLoaded handler.

Layout / HTML

Three stacked <section> elements, each exactly one viewport tall:

<section class="intro">
  <h1>Dynamic Cursor Trail Animation</h1>
</section>

<section class="trail-container">
  <p>( Move your cursor around and see the magic unfold )</p>
</section>

<section class="outro">
  <h1>Wrapping Up</h1>
</section>

The .trail-container section is the interactive zone: trail <img class="trail-img"> elements are created and appended directly into it at runtime. Load the script with <script type="module" src="./script.js">.

Styling

  • Global reset: * { margin: 0; padding: 0; box-sizing: border-box; }.
  • body: font-family: "PP Neue Montreal", sans-serif (any clean grotesque sans fallback is fine); background-color: #1e1e1e.
  • h1: color #fff, font-size: 5vw, font-weight: 500, user-select: none.
  • p: text-transform: uppercase, text-align: center, font-family: "Akkurat Mono", monospace (monospace fallback fine), font-weight: 600, user-select: none. Default black text.
  • Every section: position: relative, width: 100vw, height: 100vh, display: flex, justify-content: center, align-items: center, overflow: hidden (crucial — trail images spawning near the edges must be clipped by the section).
  • .trail-container: background-color: #fcfcfc (near-white panel sandwiched between the two dark sections).
  • .trail-img: position: absolute, width: 200px, height: 200px, object-fit: cover, border-radius: 4px, transform-origin: center, pointer-events: none, will-change: transform.

Animation engine (be precise — no GSAP, no ScrollTrigger)

Config constants
imageCount: 35            // pool of image URLs
imageLifespan: 750        // ms an image lives before its exit starts
removalDelay: 50          // ms minimum between two consecutive removals
mouseThreshold: 100       // px of cursor travel needed to spawn a new image
scrollThreshold: 50       // ms throttle between scroll-spawned images
idleCursorInterval: 300   // ms between spawns while the cursor rests in the panel
inDuration: 750           // ms entrance transition
outDuration: 1000         // ms exit transition
inEasing:  cubic-bezier(.07, .5, .5, 1)    // fast start, soft settle
outEasing: cubic-bezier(.87, 0, .13, 1)    // aggressive ease-in-out

Build an array of 35 image URLs (img1 … img35). Keep a trail queue of live images, each entry { element, rotation, removeTime }.

State

Track mouseX/mouseY, lastMouseX/lastMouseY (position of the last spawn), booleans isMoving, isCursorInContainer, isScrolling, scrollTicking, and timestamps lastRemovalTime, lastSteadyImageTime, lastScrollTime.

isInContainer(x, y) compares viewport coordinates against container.getBoundingClientRect() (left/right/top/bottom inclusive).

Seed the initial cursor position with a one-shot mouseover listener on document: on first fire, set mouseX/Y and lastMouseX/Y from event.clientX/clientY, compute isCursorInContainer, then remove the listener.

Spawning an image — createImage()
  1. Create an <img> with class trail-img; pick a random URL from the pool.
  2. Random rotation: (Math.random() - 0.5) * 50 → uniform in −25°…+25°.
  3. Position it relative to the container: left = mouseX - rect.left, top = mouseY - rect.top (rect from getBoundingClientRect()), in px.
  4. Initial inline transform: translate(-50%, -50%) rotate(<r>deg) scale(0), with inline transition: transform 750ms cubic-bezier(.07,.5,.5,1).
  5. Append to the container, then after a 10 ms setTimeout set the transform to the same translate/rotate but scale(1) so the transition plays (scale 0 → 1 pop-in; rotation stays fixed for the image's whole life).
  6. Push { element, rotation, removeTime: Date.now() + 750 } onto the trail queue.
Per-frame spawner — createTrailImage() (called every rAF)
  • Bail if the cursor isn't inside the container.
  • If isMoving and the Euclidean distance from lastMouseX/Y to mouseX/Y exceeds 100 px: update lastMouseX/Y to the current position and spawn.
  • Else if not moving and now - lastSteadyImageTime >= 300 ms: update lastSteadyImageTime and spawn (a resting cursor keeps emitting a gentle pulse of images every 300 ms).
Scroll-driven spawner — createScrollTrailImage()

Bail if the cursor isn't in the container. Otherwise fake enough travel to force a spawn: offset lastMouseX and lastMouseY each by (mouseThreshold + 10) px with an independent random sign (±110), call createImage(), then reset lastMouseX/Y back to the real mouseX/Y.

Removal — removeOldImages() (called every rAF)
  • Throttle: return if now - lastRemovalTime < 50 ms or the queue is empty.
  • Only inspect the oldest entry (trail[0]). If now >= removeTime, shift it off the queue, overwrite its inline transition to transform 1000ms cubic-bezier(.87,0,.13,1), set its transform to translate(-50%, -50%) rotate(<same r>deg) scale(0), record lastRemovalTime, and remove the element from the DOM via setTimeout after 1000 ms (guard that it still has a parent). So images die strictly FIFO, at most one every 50 ms.
Event wiring
  • document mousemove: update mouseX/Y and isCursorInContainer. When inside the container set isMoving = true and debounce it back to false after 100 ms of no movement (clear/reset a timeout each event).
  • window scroll (passive) — listener 1: recompute isCursorInContainer from the stored mouse position; if inside, set isMoving = true, nudge lastMouseX by (Math.random() - 0.5) * 10, and debounce isMoving = false after 100 ms.
  • window scroll (passive) — listener 2: set isScrolling = true; throttle to one pass per 50 ms (lastScrollTime); use a scrollTicking flag so only one requestAnimationFrame callback is queued at a time — inside it, if still scrolling, call createScrollTrailImage() and clear isScrolling, then release the tick flag.
  • Main loop: const animate = () => { createTrailImage(); removeOldImages(); requestAnimationFrame(animate); }; animate();
  • Smooth scroll: new Lenis({ autoRaf: true }) — no other Lenis config.

Assets / images

35 editorial fashion/beauty portraits (moody studio photography — dramatic lighting, bold styling, mixed dark and vividly colored backdrops). Any source aspect works since each is cropped to a 200×200 square by object-fit: cover. Reference them as img1.jpegimg35.jpeg (adjust paths/extensions to whatever assets are available).

Behavior notes

  • Desktop / pointer-driven experience: without a mouse nothing spawns (no touch handling).
  • The trail self-cleans: with a 750 ms lifespan, ~1 s exit and FIFO removal throttled at 50 ms, a fast-moving cursor keeps roughly a dozen images alive at once.
  • Because images are absolutely positioned children of the section with overflow: hidden, they scroll with the panel and clip at its edges.
  • No console errors on load; the effect needs no user interaction to initialize, only to display.