An interactive cursor effect that responds to movement with layered visuals. Each motion leaves a soft trail of images that fade and scale in rhythm, creating a playful yet refined experience.
<!-- GSAP core (add this once per project) -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
.ou-image-trail {
position: relative;
overflow: hidden;
}
.ou-image-trail-wrap {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
isolation: isolate;
will-change: transform;
}
.ou-trail-img {
width: 200px;
aspect-ratio: 3/4;
position: absolute;
opacity: 0;
border-radius: 14px;
pointer-events: none;
user-select: none;
will-change: transform, opacity;
transform-origin: center;
}
.ou-trail-img img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: inherit;
display: block;
}
(() => {
// Image sources for the trail (swap or expand as needed)
const IMG_URLS = [
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-1.webp",
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-2.webp",
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-3.webp",
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-4.webp",
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-7.webp",
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-6.webp",
"https://showcase.oura.supply/wp-content/uploads/2025/10/Cursor-Image-Trail-8.webp"
];
const THRESHOLD = 120, // px movement before spawning new image
VISIBLE_TIME = 0.5; // fade-out delay (sec)
const spawnTrail = (wrap, x, y, src) => {
const el = document.createElement("div");
el.className = "ou-trail-img";
el.innerHTML = `<img src="${src}" alt="" loading="lazy">`;
wrap.appendChild(el);
gsap.timeline({ onComplete: () => el.remove() })
.set(el, { x: x - 70, y: y - 70, zIndex: Date.now() })
.fromTo(el, { opacity: 0, scale: 0.5 }, { opacity: 1, scale: 1, duration: 0.3, ease: "power3.out" })
.to(el, { opacity: 0, scale: 0, duration: 0.6, ease: "power1.inOut" }, VISIBLE_TIME);
};
const initTrail = (section) => {
if (section.dataset.trailInit) return;
section.dataset.trailInit = "1";
const wrap = Object.assign(document.createElement("div"), { className: "ou-image-trail-wrap" });
section.appendChild(wrap);
let x = 0, y = 0, i = 0;
section.addEventListener("mousemove", e => {
const r = section.getBoundingClientRect(),
mx = e.clientX - r.left,
my = e.clientY - r.top;
if (Math.abs(x - mx) > THRESHOLD || Math.abs(y - my) > THRESHOLD) {
x = mx; y = my;
spawnTrail(wrap, x, y, IMG_URLS[i++ % IMG_URLS.length]);
}
});
};
const boot = s => (s || document).querySelectorAll(".ou-image-trail").forEach(initTrail);
if (window.elementorFrontend?.hooks)
jQuery(window).on("elementor/frontend/init", () => {
elementorFrontend.hooks.addAction("frontend/element_ready/global", $s => boot($s[0]));
boot(document);
});
else document.addEventListener("DOMContentLoaded", () => boot());
})();
Add the class "ou-image-trail" to the container where the effect should appear.
Some solutions only work on the live site. Always publish and test after each change, as results may not appear in the editor.