<h1>Scroll down luv</h1>
<div class="marquee" data-reversed="false">
	<span>Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
</div>
<div class="marquee" data-reversed="true">
	<span>Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
</div>
<div class="marquee" data-reversed="false">
	<span>Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
	<span aria-hidden="true">Oi, oi!</span>
</div>
body {
	padding: 25vh 0 50vh 0;
	background: #222;
    scroll-behavior: smooth;
	font: 600 8vw/1 "Inter", sans-serif;
}

h1 {
	color: #777;
	display: block;
	font-size: 1.5rem;
	padding: 0 0 35vh 0;
	text-align: center;
	text-transform: uppercase;
}

.marquee {
	display: flex;
	padding: 15px 0;
	margin: 20px 0;
	white-space: nowrap;
	color: #DDD;
	justify-content: flex-end;
	background: #111;
	
	&[data-reversed="true"] {
		justify-content: flex-start;
	}
	
	span {
		display: block;
		padding: 0 1ch;
	}
}
View Compiled
gsap.registerPlugin(ScrollTrigger);

let direction = 1;

const duration = 8;
const marquees = document.querySelectorAll(".marquee");
const tl = gsap.timeline({
	repeat: -1,
	yoyo: false,
    onReverseComplete() { 
      this.totalTime(this.rawTime() + this.duration() * 10); // otherwise when the playhead gets back to the beginning, it'd stop. So push the playhead forward 10 iterations (it could be any number)
    }
});

marquees.forEach(marquee => {
	// This works beacause all the elements inside the marquee wrapper are exactly the same
	tl.to(marquee.querySelectorAll("span"), {
		xPercent: marquee.dataset.reversed === "true" ? -100 : 100,
		repeat: 0,
		ease: "linear",
		duration: duration
	}, "<");
});


let scroll = ScrollTrigger.create({
	onUpdate(self) {
		// Update the direction of the animation based on the direction of scroll
		if (self.direction !== direction) {
			direction *= -1;
		}
		
		// Update the animation speed (duration) based on the scroll speed
		tl.timeScale(duration * self.getVelocity() / 500);
		
		// Go back to the default duration
		gsap.to(tl, {timeScale: direction});	
	}
});
View Compiled

External CSS

  1. https://fonts.googleapis.com/css2?family=Inter:wght@600&amp;display=swap

External JavaScript

  1. https://unpkg.com/gsap@3/dist/gsap.min.js
  2. https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js