Rotating Image Trail

Category: Cursor Animations. Last updated: Aug 20, 2025

Code

index.html
html
<div data-trail-area="" class="rotating-image-trail">
  <div data-trail-collection="" class="rotating-image-trail__collection">
    <div class="rotating-image-trail__list">
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8da6d03b0b3e124448_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8d31f5065ad1b56f28_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-1.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8db16589054bdbb90c_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-2.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8df30cf8d00c003f71_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-3.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8d93dc505b20189674_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-4.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8dbae50223e708bd7d_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-5.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8d771505aebebe7fc5_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-6.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
      <div data-trail-item="" class="rotating-image-trail__item">
        <div class="rotating-image-trail__card"><img src="https://cdn.prod.website-files.com/6932993a27f964dfe176d82d/6932ce8df133e02803a28424_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-7.avif" loading="eager" alt="" class="rotating-image-trail__card-img"></div>
      </div>
    </div>
  </div>
</div>
styles.css
css
.rotating-image-trail {
  width: 100vw;
  height: 100vh;
  position: absolute;
}

.rotating-image-trail__collection {
  opacity: 0;
  pointer-events: none;
}

.rotating-image-trail__list {
  grid-column-gap: 1em;
  grid-row-gap: 1em;
  flex-flow: wrap;
  display: flex;
}

.rotating-image-trail__item {
  z-index: 10;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}

.rotating-image-trail__card {
  aspect-ratio: 3 / 4;
  width: 10vw;
  position: relative;
}

.rotating-image-trail__card-img {
  object-fit: cover;
  width: 100%;
  height: 100%;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
}

[data-trail-item="hidden"] {
  transform: translate(-50%, -50%) scale(0) rotate(-20deg);
  position: absolute;
}

[data-trail-item="visible"] {
  transform: translate(-50%, -50%) scale(1) rotate(0.001deg);
  transition: transform 0.4s cubic-bezier(0.625, 0.05, 0, 1);
  position: absolute;
}

[data-trail-item="transition-out"] {
  transform: translate(-50%, -50%) scale(0) rotate(180deg);
  transition: transform 0.8s cubic-bezier(0.625, 0, 0.875, 0);
  position: absolute;
}

[data-trail-item="hidden"] {
  transform: translate(-50%, -50%) scale(0) rotate(-20deg);
  position: absolute;
}

[data-trail-item="visible"] {
  transform: translate(-50%, -50%) scale(1) rotate(0.001deg);
  transition: transform 0.4s cubic-bezier(0.625, 0.05, 0, 1);
  position: absolute;
}

[data-trail-item="transition-out"] {
  transform: translate(-50%, -50%) scale(0) rotate(180deg);
  transition: transform 0.8s cubic-bezier(0.625, 0, 0.875, 0);
  position: absolute;
}
script.js
javascript
function initRotatingImageTrail() {
  var area = document.querySelector("[data-trail-area]");
  if (!area) return;

  var collection = area.querySelector("[data-trail-collection]");
  if (!collection) return;

  var items = collection.querySelectorAll("[data-trail-item]");
  if (!items.length) return;

  // Distance logic
  var index = 0;
  var lastCloneX = null;
  var lastCloneY = null;

  var cardWidth = items[0].getBoundingClientRect().width;
  var stepDistance = cardWidth * 0.5;

  function spawnTrailItem(x, y) {
    var original = items[index];
    var clone = original.cloneNode(true);

    clone.style.left = x + "px";
    clone.style.top = y + "px";

    clone.setAttribute("data-trail-item", "hidden");

    area.appendChild(clone);

    void clone.getBoundingClientRect();

    clone.setAttribute("data-trail-item", "visible");

    setTimeout(function () {
      clone.setAttribute("data-trail-item", "transition-out");
    }, 400);

    setTimeout(function () {
      clone.remove();
    }, 1200);

    index = (index + 1) % items.length;
    lastCloneX = x;
    lastCloneY = y;
  }

  // Mouse movement logic
  area.addEventListener("mousemove", function (event) {
    var rect = area.getBoundingClientRect();
    var x = event.clientX - rect.left;
    var y = event.clientY - rect.top;

    if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
      lastCloneX = null;
      lastCloneY = null;
      return;
    }

    if (lastCloneX === null || lastCloneY === null) {
      spawnTrailItem(x, y);
      return;
    }

    var dx = x - lastCloneX;
    var dy = y - lastCloneY;
    var distance = Math.sqrt(dx * dx + dy * dy);

    if (distance >= stepDistance) {
      spawnTrailItem(x, y);
    }
  });
}

// Initialize Rotating Image Trail
document.addEventListener("DOMContentLoaded", function () {
  initRotatingImageTrail();
});

Guide

Implementation

Area

Use [data-trail-area] to define the hoverable region that listens to mouse movement and receives the spawned trail clones.

Collection

Use [data-trail-collection] to hold the original items that the script cycles through when creating each trail clone.

Item

Use [data-trail-item] on each card you want to be eligible for cloning, with the script rotating through them in order for every spawn.

State

[data-trail-item="hidden"] to mark a freshly appended clone before it is visually activated so your CSS can set its initial state.[data-trail-item="visible"] to switch the clone into its active on screen state right after layout is forced.[data-trail-item="transition-out"] to trigger the clone exit animation shortly after it becomes visible, before removal.

Distance

Use the first [data-trail-item] width as the spacing basis, because the script checks how far the cursor has moved before spawning the next clone, and you can tweak the feel of the trail by changing the multiplier in the JavaScript.

Timing

Use the timeout values to control how long a clone stays on screen and how quickly it transitions out, and you can fine tune the rhythm of the trail by adjusting these numbers directly in the JavaScript.