Web Animations Medium
Velocity Marquee
Infinite marquee whose speed reacts to Lenis scroll velocity for a tactile editorial band.
Open in Lab
MCP
html css gsap lenis javascript
Targets: JS HTML
Code
[hidden] {
display: none !important;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #0e0e12;
color: #eceaf2;
}
.copy {
min-height: 50vh;
display: grid;
place-content: center;
text-align: center;
padding: 2rem;
}
.copy.tall {
min-height: 120vh;
}
.copy h1 {
margin: 0 0 0.5rem;
}
.copy p {
margin: 0;
color: #8a8696;
}
.marquee-wrap {
overflow: hidden;
border-block: 1px solid rgba(255, 255, 255, 0.08);
background: #16161c;
padding: 1.25rem 0;
}
.marquee {
display: flex;
width: max-content;
white-space: nowrap;
font-size: clamp(1.25rem, 4vw, 2rem);
font-weight: 600;
letter-spacing: -0.02em;
text-transform: uppercase;
will-change: transform;
}
.marquee span {
padding-right: 2rem;
}function whenGsapReady(run) {
const wait = () => {
if (!window.gsap) {
requestAnimationFrame(wait);
return;
}
run();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", wait);
} else {
wait();
}
}
whenGsapReady(() => {
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const marquee = document.querySelector("[data-marquee]");
if (!marquee) return;
const loopWidth = () => marquee.scrollWidth / 2;
let offset = 0;
let velocityBoost = 0;
const basePxPerFrame = 1.8;
if (reduced) return;
if (typeof Lenis !== "undefined") {
const lenis = new Lenis({ autoRaf: false, smoothWheel: true });
lenis.on("scroll", ({ velocity }) => {
velocityBoost = velocity * 10;
});
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
}
gsap.ticker.add(() => {
const ratio = gsap.ticker.deltaRatio();
velocityBoost *= 0.9;
const speed = basePxPerFrame + Math.min(12, Math.abs(velocityBoost));
offset -= speed * ratio;
const w = loopWidth();
if (w <= 0) return;
if (offset <= -w) offset += w;
gsap.set(marquee, { x: offset, force3D: true });
});
});<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Velocity Marquee</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<section class="copy">
<h1>Scroll fast</h1>
<p>The ticker below speeds up with your scroll velocity.</p>
</section>
<div class="marquee-wrap" aria-hidden="true">
<div class="marquee" data-marquee>
<span>Steal patterns · Ship faster · Scroll-driven motion · </span>
<span>Steal patterns · Ship faster · Scroll-driven motion · </span>
</div>
</div>
<section class="copy tall">
<p>Keep scrolling to feel the band accelerate and ease back.</p>
</section>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lenis.min.js"></script>
<script src="script.js"></script>
</body>
</html>Velocity Marquee
Base drift runs on gsap.ticker; scroll velocity from Lenis adds or subtracts speed so the band feels physically linked to the page.