12 CSS Infinite Marquee Designs12 / 12

CSS + JSMIT licensed

Scroll-Velocity Reactive Marquee — Speed & Skew Follow the Page

The award-site flourish: a giant outlined-text marquee that idles at a lazy drift, then accelerates, skews and even reverses with the user's scroll — scroll down fast and the words whip left with a speed-lean; flick up and they swing back. One rAF loop, one scroll listener, and lerp smoothing doing all the choreography.

Published

Live Demo
Try it

The code

<section class="mqs-12" aria-label="Scroll velocity marquee demo">
  <p class="mqs-12__hint">Scroll this page — fast, slow, then back up ↑</p>
  <div class="mqs-12__stage">
    <div class="mqs-12__viewport">
      <div class="mqs-12__track" id="mqs-12-track">
        <span class="mqs-12__word">Velocity</span><span class="mqs-12__word mqs-12__word--ghost">Motion</span><span class="mqs-12__word">Design</span><span class="mqs-12__word mqs-12__word--ghost">Craft</span>
        <span class="mqs-12__word" aria-hidden="true">Velocity</span><span class="mqs-12__word mqs-12__word--ghost" aria-hidden="true">Motion</span><span class="mqs-12__word" aria-hidden="true">Design</span><span class="mqs-12__word mqs-12__word--ghost" aria-hidden="true">Craft</span>
      </div>
    </div>
  </div>
  <p class="mqs-12__hint">Notice it reverses when you scroll up</p>
</section>
.mqs-12,
.mqs-12 *,
.mqs-12 *::before,
.mqs-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.mqs-12 {
  width: 100%;
  min-height: 100vh;
  --gap: 48px;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #101014;
  padding-block: 0 200vh;
}

.mqs-12__hint {
  text-align: center;
  font-size: 12px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: #52525b;
  padding-block: 24px;
}

.mqs-12__stage {
  position: sticky;
  top: 15vh;
  padding-block: 30px;
  border-block: 1px solid #27272a;
  background: #101014;
}

.mqs-12__viewport {
  overflow: hidden;
}

.mqs-12__track {
  display: flex;
  align-items: baseline;
  gap: var(--gap);
  width: max-content;
  will-change: transform;
}

.mqs-12__word {
  flex: none;
  font-size: clamp(56px,9vw,120px);
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1;
  color: #f4f4f5;
}

.mqs-12__word--ghost {
  color: transparent;
  -webkit-text-stroke: 2px #3f3f46;
}

@media (prefers-reduced-motion: reduce) {
  .mqs-12 {
    width: 100%;
    padding-block: 64px;
  }
}
// The scroll listener only RECORDS velocity; rAF does all the writing (never write transforms in a scroll handler).
const track = document.getElementById('mqs-12-track');
const REDUCED = matchMedia('(prefers-reduced-motion: reduce)').matches;
const BASE = 60;      // idle drift px/s
const FACTOR = 0.25;  // how hard page scroll drives the marquee
let x = 0, half = 0, velocity = 0, smooth = 0;
let lastScroll = window.scrollY, prev = performance.now();

const measure = () => { half = (track.scrollWidth + parseFloat(getComputedStyle(track).gap)) / 2; };
new ResizeObserver(measure).observe(track); measure();

if (!REDUCED) {
  window.addEventListener('scroll', () => {
    velocity += (window.scrollY - lastScroll) * 12;   // signed: up-scroll reverses the marquee
    lastScroll = window.scrollY;
  }, { passive: true });

  requestAnimationFrame(function frame(now) {
    const dt = Math.min((now - prev) / 1000, 0.05); prev = now;
    velocity *= 0.9;                                   // decay: effect relaxes when scrolling stops
    smooth += (velocity - smooth) * 0.08;              // lerp: spiky wheel events become liquid motion
    x += (BASE + smooth * FACTOR) * dt;
    if (half > 0) x = ((x % half) + half) % half;      // modulo wrap = no seam at any speed
    const skew = Math.max(-12, Math.min(12, smooth * -0.02));
    track.style.transform = 'translate3d(' + (-x) + 'px,0,0) skewX(' + skew + 'deg)';
    requestAnimationFrame(frame);
  });
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Infinite Marquee Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Scroll-Velocity Reactive Marquee — Speed & Skew Follow the Page
Source: https://codefronts.com/motion/css-infinite-marquee/scroll-velocity-reactive-marquee/

The award-site flourish: a giant outlined-text marquee that idles at a lazy drift, then accelerates, skews and even reverses with the user's scroll — scroll down fast and the words whip left with a speed-lean; flick up and they swing back. One rAF loop, one scroll listener, and lerp smoothing doing all the choreography.
## HTML
```html
<section class="mqs-12" aria-label="Scroll velocity marquee demo">
  <p class="mqs-12__hint">Scroll this page — fast, slow, then back up ↑</p>
  <div class="mqs-12__stage">
    <div class="mqs-12__viewport">
      <div class="mqs-12__track" id="mqs-12-track">
        <span class="mqs-12__word">Velocity</span><span class="mqs-12__word mqs-12__word--ghost">Motion</span><span class="mqs-12__word">Design</span><span class="mqs-12__word mqs-12__word--ghost">Craft</span>
        <span class="mqs-12__word" aria-hidden="true">Velocity</span><span class="mqs-12__word mqs-12__word--ghost" aria-hidden="true">Motion</span><span class="mqs-12__word" aria-hidden="true">Design</span><span class="mqs-12__word mqs-12__word--ghost" aria-hidden="true">Craft</span>
      </div>
    </div>
  </div>
  <p class="mqs-12__hint">Notice it reverses when you scroll up</p>
</section>
```
## CSS
```css
.mqs-12,
.mqs-12 *,
.mqs-12 *::before,
.mqs-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.mqs-12 {
  width: 100%;
  min-height: 100vh;
  --gap: 48px;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #101014;
  padding-block: 0 200vh;
}

.mqs-12__hint {
  text-align: center;
  font-size: 12px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: #52525b;
  padding-block: 24px;
}

.mqs-12__stage {
  position: sticky;
  top: 15vh;
  padding-block: 30px;
  border-block: 1px solid #27272a;
  background: #101014;
}

.mqs-12__viewport {
  overflow: hidden;
}

.mqs-12__track {
  display: flex;
  align-items: baseline;
  gap: var(--gap);
  width: max-content;
  will-change: transform;
}

.mqs-12__word {
  flex: none;
  font-size: clamp(56px,9vw,120px);
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1;
  color: #f4f4f5;
}

.mqs-12__word--ghost {
  color: transparent;
  -webkit-text-stroke: 2px #3f3f46;
}

@media (prefers-reduced-motion: reduce) {
  .mqs-12 {
    width: 100%;
    padding-block: 64px;
  }
}
```

## JavaScript
```js
// The scroll listener only RECORDS velocity; rAF does all the writing (never write transforms in a scroll handler).
const track = document.getElementById('mqs-12-track');
const REDUCED = matchMedia('(prefers-reduced-motion: reduce)').matches;
const BASE = 60;      // idle drift px/s
const FACTOR = 0.25;  // how hard page scroll drives the marquee
let x = 0, half = 0, velocity = 0, smooth = 0;
let lastScroll = window.scrollY, prev = performance.now();

const measure = () => { half = (track.scrollWidth + parseFloat(getComputedStyle(track).gap)) / 2; };
new ResizeObserver(measure).observe(track); measure();

if (!REDUCED) {
  window.addEventListener('scroll', () => {
    velocity += (window.scrollY - lastScroll) * 12;   // signed: up-scroll reverses the marquee
    lastScroll = window.scrollY;
  }, { passive: true });

  requestAnimationFrame(function frame(now) {
    const dt = Math.min((now - prev) / 1000, 0.05); prev = now;
    velocity *= 0.9;                                   // decay: effect relaxes when scrolling stops
    smooth += (velocity - smooth) * 0.08;              // lerp: spiky wheel events become liquid motion
    x += (BASE + smooth * FACTOR) * dt;
    if (half > 0) x = ((x % half) + half) % half;      // modulo wrap = no seam at any speed
    const skew = Math.max(-12, Math.min(12, smooth * -0.02));
    track.style.transform = 'translate3d(' + (-x) + 'px,0,0) skewX(' + skew + 'deg)';
    requestAnimationFrame(frame);
  });
}
```

How this works

Three numbers run the show. A scroll listener records the per-event delta into a raw velocity; the rAF loop (1) eases that raw value toward zero (velocity *= 0.9 — scroll stops, effect relaxes), (2) lerps a smooth copy toward it so spiky wheel events become liquid motion, and (3) advances the marquee offset by (BASE + smooth × FACTOR) × dt. Because smooth is signed, scrolling up doesn't just slow the marquee — it reverses it, which is the moment users notice the page is 'alive'.

The skew is the same signal reused: skewX(clamp(smooth × −0.02, −12, 12)deg) on the track makes the type lean into its motion like italics under acceleration. Offset wraps with the modulo-over-half-width trick from demo 11, so no seam at any speed. The text itself is display-scale outlined type via -webkit-text-stroke with a filled copy alternating — pure fashion, zero extra tech.

Everything is transform-only on one composited track, so even violent scrolling costs one transform update per frame. Under prefers-reduced-motion the listener never attaches and BASE drops to 0 — the strip becomes a static typographic band.

Techniques used: scroll-delta velocity capture with decay + lerp smoothing · signed velocity → speed AND direction · velocity-mapped skewX with clamp · modulo wrap seamless offset · -webkit-text-stroke outline typography · single composited transform per frame · reduced-motion = static band (no listener at all)

Make it yours

  • Sensitivity: FACTOR (0.25 here) — how hard page scroll drives the marquee; 0.5+ is chaotic, 0.1 is subliminal.
  • Lean: the −0.02 skew multiplier and ±12deg clamp; set 0 to keep acceleration without the italics.
  • Idle drift: BASE px/s — 0 makes the marquee move ONLY when the page scrolls (a strong minimal look).
  • Smoothing: the 0.08 lerp — lower is dreamier, higher is twitchier.

Gotchas — read before shipping

  • Never write transforms inside the scroll event — wheel events fire faster than frames; the listener only records the delta, rAF does all writing.
  • Clamp the skew: unclamped fast scrolls shear the text into unreadable parallelograms.
  • This demo needs page-scroll room to feel — it ships with tall padding; embedded in a short page it just idles.
  • Wrap width must be re-measured on resize/font-load (ResizeObserver), same as any modulo marquee.
  • Respect reduced motion by not attaching the listener at all — a scroll-reactive element that merely 'moves less' still violates the user's request.

Browser support

ChromeSafariFirefoxEdge
allallallall

rAF, scroll events and text-stroke (prefixed) are universal. The whole effect is progressive enhancement on top of a static type band.

Techniques used in this demo

Search CodeFronts

Loading…