Stacking Image Trail
Category: Cursor Animations. Last updated: Aug 20, 2025
Code
<div data-stacked-trail-area="" class="stacked-image-trail">
<div class="stacked-image-trail__collection">
<div class="stacked-image-trail__list">
<div data-stacked-trail-item="" class="stacked-image-trail__item">
<div class="stacked-image-trail__card"><img loading="eager" src="https://cdn.prod.website-files.com/693690ed25b06d7512221694/69369580facbfd3ab005ba46_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-4.avif" alt="" class="stacked-image-trail__card-img"></div>
</div>
<div data-stacked-trail-item="" class="stacked-image-trail__item">
<div class="stacked-image-trail__card"><img loading="eager" src="https://cdn.prod.website-files.com/693690ed25b06d7512221694/6936c55f040b346a1076dffa_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-7%2013.avif" alt="" class="stacked-image-trail__card-img"></div>
</div>
<div data-stacked-trail-item="" class="stacked-image-trail__item">
<div class="stacked-image-trail__card"><img loading="eager" src="https://cdn.prod.website-files.com/693690ed25b06d7512221694/6936c1a8b4235714bfcfa5fc_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-7%201.avif" alt="" class="stacked-image-trail__card-img"></div>
</div>
<div data-stacked-trail-item="" class="stacked-image-trail__item">
<div class="stacked-image-trail__card"><img loading="eager" src="https://cdn.prod.website-files.com/693690ed25b06d7512221694/69369580facbfd3ab005ba42_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-6.avif" alt="" class="stacked-image-trail__card-img"></div>
</div>
<div data-stacked-trail-item="" class="stacked-image-trail__item">
<div class="stacked-image-trail__card"><img loading="eager" src="https://cdn.prod.website-files.com/693690ed25b06d7512221694/69369580facbfd3ab005ba4e_QmdGkQSqqBC5iYSaebPsFoaSWtjhaMQNWpVnWoGJeATp2h-5.avif" alt="" class="stacked-image-trail__card-img"></div>
</div>
</div>
</div>
</div>.stacked-image-trail {
width: 100%;
height: 100%;
position: absolute;
top: 0%;
left: 0%;
}
.stacked-image-trail__collection {
pointer-events: none;
width: 100%;
height: 100%;
}
.stacked-image-trail__list {
grid-column-gap: 1em;
grid-row-gap: 1em;
flex-flow: wrap;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
display: flex;
}
.stacked-image-trail__item {
z-index: 10;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
position: absolute;
top: 50%;
left: 50%;
}
[data-stacked-trail-item] {
transform: translate(-50%, -50%) rotate(0.001deg) scale(0.5);
transition: transform 0.8s cubic-bezier(0.87, 0, 0.13, 1), clip-path 0.8s cubic-bezier(0.87, 0, 0.13, 1);
clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%);
}
[data-stacked-trail-area="hover"] [data-stacked-trail-item] {
transform: translate(-50%, -50%) rotate(0.001deg) scale(1);
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
.stacked-image-trail__card {
aspect-ratio: 3 / 4;
width: 15vw;
position: relative;
}
.stacked-image-trail__card-img {
object-fit: cover;
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0;
left: 0;
}
[data-stacked-trail-item] {
transform: translate(-50%, -50%) rotate(0.001deg) scale(0.5);
transition: transform 0.8s cubic-bezier(0.87, 0, 0.13, 1), clip-path 0.8s cubic-bezier(0.87, 0, 0.13, 1);
clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%);
}
[data-stacked-trail-area="hover"] [data-stacked-trail-item] {
transform: translate(-50%, -50%) rotate(0.001deg) scale(1);
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
/* Optional: Stack in Webflow Designer */
:is(.wf-design-mode, .wf-editor) [data-stacked-trail-item] {
transform: translate(-50%, -50%) rotate(0.001deg) scale(1);
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
:is(.wf-design-mode, .wf-editor) [data-stacked-trail-item]:nth-child(5) {
transform: translate(0%, -30%) rotate(0.001deg) scale(1);
z-index: 1;
}
:is(.wf-design-mode, .wf-editor) [data-stacked-trail-item]:nth-child(4) {
transform: translate(-25%, -40%) rotate(0.001deg) scale(1);
z-index: 2;
}
:is(.wf-design-mode, .wf-editor) [data-stacked-trail-item]:nth-child(3) {
z-index: 3;
}
:is(.wf-design-mode, .wf-editor) [data-stacked-trail-item]:nth-child(2) {
transform: translate(-75%, -60%) rotate(0.001deg) scale(1);
z-index: 4;
}
:is(.wf-design-mode, .wf-editor) [data-stacked-trail-item]:nth-child(1) {
transform: translate(-100%, -70%) rotate(0.001deg) scale(1);
z-index: 5;
}function initStackedImageTrail() {
var areas = Array.from(document.querySelectorAll("[data-stacked-trail-area]"));
if (!areas.length) return;
var leadEase = 0.25;
var trailEase = 0.16;
var pathFollow = 1;
var instances = [];
areas.forEach(function (area) {
var cards = Array.from(area.querySelectorAll("[data-stacked-trail-item]"));
if (!cards.length) return;
var mouseX = 50;
var mouseY = 50;
var lastClientX = null;
var lastClientY = null;
var isHovering = false;
var states = cards.map(function (card, index) {
card.style.zIndex = cards.length - index;
return {
el: card,
x: 50,
y: 50
};
});
function getPercentFromClient(clientX, clientY) {
var rect = area.getBoundingClientRect();
var x = ((clientX - rect.left) / rect.width) * 100;
var y = ((clientY - rect.top) / rect.height) * 100;
if (x < 0) x = 0;
if (x > 100) x = 100;
if (y < 0) y = 0;
if (y > 100) y = 100;
return { x: x, y: y };
}
function updateFromPointer() {
if (lastClientX === null || lastClientY === null) return;
var rect = area.getBoundingClientRect();
var inside =
lastClientX >= rect.left &&
lastClientX <= rect.right &&
lastClientY >= rect.top &&
lastClientY <= rect.bottom;
if (inside && !isHovering) {
isHovering = true;
area.setAttribute("data-stacked-trail-area", "hover");
} else if (!inside && isHovering) {
isHovering = false;
area.setAttribute("data-stacked-trail-area", "");
}
if (!inside) return;
var pos = getPercentFromClient(lastClientX, lastClientY);
mouseX = pos.x;
mouseY = pos.y;
}
function handleDocumentMouseMove(evt) {
lastClientX = evt.clientX;
lastClientY = evt.clientY;
updateFromPointer();
}
function handleScroll() {
updateFromPointer();
}
document.addEventListener("mousemove", handleDocumentMouseMove);
window.addEventListener("scroll", handleScroll);
if (area.matches(":hover")) {
isHovering = true;
area.setAttribute("data-stacked-trail-area", "hover");
}
function step() {
states.forEach(function (state, index) {
var targetX;
var targetY;
if (index === 0) {
targetX = mouseX;
targetY = mouseY;
} else {
var prev = states[index - 1];
var followX = prev.x;
var followY = prev.y;
targetX = followX * pathFollow + mouseX * (1 - pathFollow);
targetY = followY * pathFollow + mouseY * (1 - pathFollow);
}
var ease = index === 0 ? leadEase : trailEase;
state.x += (targetX - state.x) * ease;
state.y += (targetY - state.y) * ease;
state.el.style.left = state.x + "%";
state.el.style.top = state.y + "%";
});
}
instances.push({
step: step
});
});
if (!instances.length) return;
function animate() {
instances.forEach(function (instance) {
instance.step();
});
requestAnimationFrame(animate);
}
animate();
}
// Initialize Stacked Image Trail
document.addEventListener("DOMContentLoaded", function () {
initStackedImageTrail();
});Guide
Implementation
Area
Use [data-stacked-trail-area] to define an independent interactive region where the stacked trail effect activates and tracks cursor movement only inside this specific area.
Item
Use [data-stacked-trail-item] to register each element in the stack that should follow the cursor, giving every item its own stored x and y position while easing toward its target in sequence.
Customize
Use var leadEase = 0.25; to control how responsively the first item reacts to cursor movement, acting as the leading point of the animation. Use var trailEase = 0.16; to adjust how smoothly the rest of the items drift behind the leader, giving the stack its trailing softness. Use var pathFollow = 1; to set how closely items follow the exact path of the one above them, blending between direct cursor following and chained motion.
Hover State
Use [data-stacked-trail-area="hover"] to trigger CSS-driven scale or visual effects, as the script automatically toggles this attribute when the cursor enters or leaves the area.