Infinite Marquee
A lightweight, glitch-free infinite marquee component delivered via CDN — built for Webflow and any no-code platform.

What is Infinite Marquee?
Infinite Marquee is a drop-in component that creates smooth, seamless scrolling text or content loops. You add a CSS file, a JS file, write a few HTML attributes, and you have a production-ready marquee that pauses on hover, scales to any content, and runs without a single frame skip.
It's published to npm as @rtstic-dev/infinite-marquee and served through jsDelivr, so you can load it on any platform that accepts a script tag — Webflow, Framer, Squarespace, Shopify, or plain HTML.
The entire implementation is under 100 lines of TypeScript and 30 lines of CSS. No dependencies. No framework. Just a function that duplicates your content, sets two CSS variables, and lets a translateX animation do the rest.
The Problem
Infinite marquees are one of those things that should be simple but almost never are. The concept is straightforward: take a row of content and scroll it horizontally in a loop, forever. But the implementations you find in the wild are consistently broken in one way or another.
The most common approach is to use a CSS animation that translates the element by some fixed pixel value. This works until the content changes size, the viewport resizes, or the items aren't all the same width. Then the loop stutters, jumps, or shows a visible gap where the content wraps around.
JavaScript-based solutions try to fix this by calculating widths at runtime and repositioning elements. But they introduce their own problems — janky frame rates, layout thrashing, dependency on requestAnimationFrame timing, and scroll positions that drift over time. Some rely on cloning nodes into the DOM repeatedly, which gets expensive fast.
There are plenty of marquee libraries on npm. Most of them are React-specific, which rules them out for no-code platforms. The ones that aren't React-specific tend to be over-engineered — hundreds of lines, configuration objects with dozens of options, resize observers, intersection observers, mutation observers. All for a scrolling row of text.
The core issue is that nobody has settled on a clean, minimal pattern. The problem keeps getting re-solved with increasing complexity, and the simple CSS-first approach that actually works gets buried under layers of JavaScript that shouldn't be necessary.
Infinite Marquee takes the opposite approach. It duplicates the content a fixed number of times so there's always enough to fill the viewport, calculates the animation duration per copy using CSS variables, and lets the browser's compositor handle the rest. No resize listeners. No layout calculations. No frame-by-frame positioning. Just a translateX that moves exactly one copy's width, creating a perfect loop every time.
How It Works
The HTML Contract
The component uses three custom attributes to identify its parts. The rtstic-marquee="wrapper" goes on the outer container and handles overflow clipping. The rtstic-marquee="list" goes on the flex container that holds the items and receives the animation. Each rtstic-marquee="item" wraps an individual content piece and prevents it from shrinking or wrapping.
<div rtstic-marquee="wrapper">
<div rtstic-marquee="list">
<div rtstic-marquee="item">Item 1</div>
<div rtstic-marquee="item">Item 2</div>
<div rtstic-marquee="item">Item 3</div>
</div>
</div>This is the entire API surface. No classes to memorize, no data attributes with encoded JSON, no wrapper-within-wrapper nesting. Three attributes, one structure.
The Duplication Strategy
When initializeMarquee() runs, it reads the inner HTML of the list element and duplicates it a configurable number of times (default: 4). This creates enough copies that the content always overflows the viewport, which is what makes the loop seamless — by the time one copy scrolls out of view, the next identical copy is already in position.
The function then sets two CSS custom properties on the list element: --multiplier (how many copies exist) and --duration (the total animation time divided by the multiplier). These drive the CSS animation.
The CSS Animation
The animation itself is five lines. The list element gets display: flex and width: max-content so it sizes to its actual content. A translateX keyframe moves it from 0 to calc(-100% / var(--multiplier)) — exactly one copy's width. Because every copy is identical, this creates a seamless loop regardless of what the content looks like or how wide it is.
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-100% / var(--multiplier)));
}
}The will-change: transform property tells the browser to composite this animation on the GPU, which keeps it locked at 60fps without touching the main thread. Hover pausing is a single CSS rule that sets animation-play-state: paused.
Configuration
The defaults work for most cases, but you can pass a config object to customize behavior:
initializeMarquee({
multiplier: 5, // Number of content duplications (default: 4)
duration: 80, // Total animation cycle in seconds (default: 100)
selector: "[rtstic-marquee='list']",
});Higher multiplier values create a denser fill, which matters for short content on wide screens. Lower duration values make the scroll faster. The selector can be changed if you need to avoid attribute conflicts, but the default covers every standard use case.
Getting Started
Via CDN
The fastest way. Add two tags to your page — one for styles, one for the script:
<link
href="https://cdn.jsdelivr.net/npm/@rtstic-dev/infinite-marquee@latest/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<script
defer
src="https://cdn.jsdelivr.net/npm/@rtstic-dev/infinite-marquee@latest/dist/index.js"
></script>The script auto-initializes on DOM ready. Add the HTML structure with the three attributes, and the marquee starts scrolling immediately. No configuration needed.
For Webflow, paste the link tag in your project's Custom Code head section and the script tag in the footer section. For page-specific marquees, use the page-level custom code settings instead.
Via NPM
If you're working in a JavaScript project with a bundler:
npm install @rtstic-dev/infinite-marqueeThen import and initialize:
import { initializeMarquee } from "@rtstic-dev/infinite-marquee";
import "@rtstic-dev/infinite-marquee/dist/styles.css";
initializeMarquee();Pinning a Version
The @latest tag in the CDN URL always points to the newest published version. For production sites, pin to a specific version to avoid unexpected changes:
<script
defer
src="https://cdn.jsdelivr.net/npm/@rtstic-dev/infinite-marquee@0.0.3/dist/index.js"
></script>Tech Stack
The source is TypeScript, bundled with esbuild into a single JS file and a single CSS file. Changesets handles versioning and changelog generation. The package is published to npm and automatically available through jsDelivr's CDN. There are zero runtime dependencies — the entire dist output is just the compiled marquee code.
Why I Built This
Every Webflow project I worked on eventually needed a marquee. And every time, I'd either write one from scratch or pull in a library that did too much. The from-scratch versions worked but weren't reusable. The libraries worked but came with 50KB of JavaScript for something that should be 2KB.
I wanted a single component I could drop into any project with a CDN link — no build step on the consuming side, no framework dependency, no configuration ceremony. Just paste two tags and write three HTML attributes. Infinite Marquee is that component. It's small enough to audit in five minutes and reliable enough that I haven't had to touch it since publishing.