Marquee Vertical Item Progress

Marquee
Loop
GSAP
JS
CMS
Last Updated: Dec 4, 2025
  • gap and padding-bottom on the marquee-3_list must match for the list to loop seamlessly
  • The second marquee-3_panel has an attribute of aria-hidden="true" so duplicated content isn't read by screen readers
  • When images are inside a marquee, they should be set to eager load to avoid flicker
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
	document.querySelectorAll(".marquee-3_component").forEach((component) => {
		if (component.hasAttribute("data-marquee-3")) return;
		component.setAttribute("data-marquee-3", "");
		
		gsap.context(() => {
			let marquee = gsap.timeline({ repeat: -1 });
			marquee.fromTo(".marquee-3_track", { yPercent: 0 }, { yPercent: -50, duration: 10, ease: "none" });
		}, component);
    
		component.querySelectorAll(".marquee-3_item").forEach((item) => {
			gsap.context(() => {
				let tl = gsap.timeline({ 
					paused: true,
					defaults: { ease: "none" }
				});
				tl.fromTo(item, { x: "8em" }, { x: "0em", ease: "power1.out" });
				tl.fromTo(item, { opacity: 0.2 }, { opacity: 1, ease: "none" }, "<");
        
				tl.to(item, { x: "8em", ease: "power1.in" });
				tl.to(item, { opacity: 0.2, ease: "none" }, "<");
        
				function animate() {
					const rect = item.getBoundingClientRect();
					const compRect = component.getBoundingClientRect();
					let progress = Math.max(0, Math.min(1, (compRect.bottom - rect.top) / (rect.height + compRect.height)));
					tl.progress(progress);
					requestAnimationFrame(animate);
				}
				requestAnimationFrame(animate);

			}, item);
		});
	});
});
</script>