<section class="demo">
<div class="demo__stack">
<div class="demo__track">
<div class="demo__row demo__row--fw">
<span>Motion</span><span class="is-outline">Scroll</span><span>Drag</span><span class="is-outline">Loop</span>
<span>Motion</span><span class="is-outline">Scroll</span><span>Drag</span><span class="is-outline">Loop</span>
</div>
</div>
<div class="demo__track">
<div class="demo__row demo__row--bw">
<span class="is-outline">Loop</span><span>Drag</span><span class="is-outline">Scroll</span><span>Motion</span>
<span class="is-outline">Loop</span><span>Drag</span><span class="is-outline">Scroll</span><span>Motion</span>
</div>
</div>
</div>
</section>.demo {
min-height: 100vh;
display: grid;
place-items: center;
background: #0c0c0e;
overflow: hidden;
font-family: "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
}
.demo__stack {
display: flex;
flex-direction: column;
gap: 18px;
width: 100%;
cursor: default;
}
.demo__track {
width: 100%;
overflow: hidden;
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.demo__row {
display: flex;
align-items: center;
gap: 48px;
width: max-content;
white-space: nowrap;
will-change: transform;
}
.demo__row span {
font-size: clamp(40px, 9vw, 88px);
font-weight: 600;
letter-spacing: -0.05em;
line-height: 1;
color: #f2f2f0;
text-transform: uppercase;
}
.demo__row span.is-outline {
color: transparent;
-webkit-text-stroke: 1px rgb(242 242 240 / 0.42);
}
@media (prefers-reduced-motion: reduce) {
.demo__row { transform: none !important; }
}const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const stack = document.querySelector(".demo__stack");
const fw = document.querySelector(".demo__row--fw");
const bw = document.querySelector(".demo__row--bw");
if (!reduce && stack && fw && bw) {
const fwWidth = fw.scrollWidth / 2;
const bwWidth = bw.scrollWidth / 2;
const tweenFw = gsap.to(fw, { x: -fwWidth, duration: 18, ease: "none", repeat: -1 });
const tweenBw = gsap.fromTo(bw, { x: -bwWidth }, { x: 0, duration: 18, ease: "none", repeat: -1 });
let hovering = false;
let boostTimer = 0;
function setScale(scale, duration) {
gsap.to([tweenFw, tweenBw], {
timeScale: scale,
duration: duration,
ease: "power2.out",
overwrite: true,
});
}
stack.addEventListener("pointerenter", function () {
hovering = true;
setScale(0, 1.15);
});
stack.addEventListener("pointerleave", function () {
hovering = false;
setScale(1, 0.95);
});
function boost() {
if (hovering) {
return;
}
setScale(2.5, 0.16);
window.clearTimeout(boostTimer);
boostTimer = window.setTimeout(function () {
if (!hovering) {
setScale(1, 1.15);
}
}, 120);
}
window.addEventListener("wheel", boost, { passive: true });
window.addEventListener("scroll", boost, { passive: true });
}