Grab-to-Pause Auto-Scroll Ticker — HTML CSS JS Snippet
Grab-to-Pause Auto-Scroll Ticker · Carousels · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="ast-wrap" id="astWrap">
<div class="ast-track" id="astTrack">
<div class="ast-chip" style="background:linear-gradient(160deg,#6366f1,#4338ca)">React</div>
<div class="ast-chip" style="background:linear-gradient(160deg,#0ea5e9,#0369a1)">Vue</div>
<div class="ast-chip" style="background:linear-gradient(160deg,#ec4899,#9d174d)">Angular</div>
<div class="ast-chip" style="background:linear-gradient(160deg,#10b981,#047857)">Svelte</div>
<div class="ast-chip" style="background:linear-gradient(160deg,#f59e0b,#b45309)">Tailwind</div>
<div class="ast-chip" style="background:linear-gradient(160deg,#8b5cf6,#5b21b6)">GSAP</div>
</div>
<p class="ast-hint">Auto-scrolls continuously — grab and drag to scrub, release to resume.</p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f6f7f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ast-wrap{width:100%;max-width:480px}
.ast-track{display:flex;gap:14px;overflow:hidden;border-radius:14px;cursor:grab;touch-action:none}
.ast-track.ast-grabbing{cursor:grabbing}
.ast-chip{flex:0 0 auto;padding:18px 26px;border-radius:12px;color:#fff;font-weight:800;font-size:14px;box-shadow:0 8px 18px rgba(15,23,42,.14);user-select:none}
.ast-hint{text-align:center;color:#9ca3af;font-size:12px;margin-top:14px}var track = document.getElementById('astTrack');
var wrap = document.getElementById('astWrap');
var SPEED = 0.6; // px per ms — continuous autoplay rate
var pos = 0;
var dragging = false;
var lastFrame = null;
var rafId = null;
var loopWidth = 0;
// Duplicate the chip set once so the track can scroll a full "copy width"
// and then jump back by exactly that width with no visible seam, producing
// an infinite loop from a finite set of elements.
var originalChips = Array.prototype.slice.call(track.children);
originalChips.forEach(function (chip) { track.appendChild(chip.cloneNode(true)); });
function measure() {
loopWidth = 0;
for (var i = 0; i < originalChips.length; i++) {
loopWidth += track.children[i].getBoundingClientRect().width + 14; // + gap
}
}
function apply() {
// Wrap pos into [0, loopWidth) so it can grow or shrink without bound while
// the visual position stays within one seamless loop cycle.
var wrapped = ((pos % loopWidth) + loopWidth) % loopWidth;
track.style.transform = 'translateX(' + (-wrapped) + 'px)';
}
function tick(now) {
if (!dragging) {
if (lastFrame !== null) pos += SPEED * (now - lastFrame);
lastFrame = now;
apply();
} else {
lastFrame = now;
}
rafId = requestAnimationFrame(tick);
}
var startX = 0, startPos = 0;
function pointerDown(e) {
dragging = true;
track.classList.add('ast-grabbing');
startX = e.touches ? e.touches[0].clientX : e.clientX;
startPos = pos;
}
function pointerMove(e) {
if (!dragging) return;
var x = e.touches ? e.touches[0].clientX : e.clientX;
pos = startPos - (x - startX);
apply();
}
function pointerUp() {
dragging = false;
track.classList.remove('ast-grabbing');
}
track.addEventListener('pointerdown', pointerDown);
window.addEventListener('pointermove', pointerMove);
window.addEventListener('pointerup', pointerUp);
track.addEventListener('touchstart', pointerDown, { passive: true });
window.addEventListener('touchmove', pointerMove, { passive: true });
window.addEventListener('touchend', pointerUp);
window.addEventListener('resize', measure);
measure();
rafId = requestAnimationFrame(tick);Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Grab-to-Pause Auto-Scroll Ticker — One Position Variable, Two Different Writers

A typical infinite ticker either auto-scrolls *or* is draggable — rarely both at once, seamlessly. This one keeps a single pos variable that two completely different code paths are allowed to write to: a requestAnimationFrame loop advances it automatically by SPEED pixels per millisecond when nobody's touching it, and a pointer/touch drag handler overwrites it directly with the cursor's position when someone is. Because both paths write to the *exact same variable*, grabbing the ticker mid-scroll and releasing it resumes autoplay from precisely wherever the drag left off — there's no separate "resume point" to calculate.
The loop trick: duplicate once, wrap the position
To scroll infinitely from a *finite* set of chips, every chip is cloned once and appended after the originals — so the track visually contains two identical copies back to back. apply() then wraps pos into the range [0, loopWidth) with ((pos % loopWidth) + loopWidth) % loopWidth (the double-modulo handles negative pos correctly, which plain % in JavaScript doesn't). The instant pos would scroll past one full copy's width, the wrap makes it jump back to the equivalent position in the first copy — and because the second copy is pixel-identical to the first, that jump is completely invisible.
Autoplay pauses itself simply by not advancing
There's no explicit "pause" state variable — the tick() loop just checks if (!dragging) before adding to pos at all. While dragging is true, the rAF loop keeps running (so it's instantly ready to resume) but skips the auto-advance line entirely, letting the drag handler be the only thing touching pos for that whole gesture.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA row of technology chips scrolls continuously and smoothly to the left, looping seamlessly forever.
- 2Click and hold anywhere on the tickerAuto-scrolling stops instantly and the ticker follows your cursor precisely.
- 3Drag left or rightScrub through the chips manually in either direction, well beyond the normal auto-scroll range.
- 4ReleaseAuto-scrolling resumes immediately from exactly wherever you released it — no jump or reset.
- 5Add a seventh chipAdd one more .ast-chip element — the clone-and-loop logic picks it up automatically on the next page load.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Change the SPEED constant (pixels per millisecond) — a larger number scrolls faster, a smaller one slower. Since it's time-based rather than frame-based, the speed stays consistent regardless of the display's refresh rate.
loopWidth is measured directly from the real rendered widths of the original chips (plus their gaps) on every measure() call — it doesn't assume equal widths, so any mix of chip sizes still wraps at exactly the right pixel offset.
Resetting position abruptly (position = 0) would produce a visible jump the instant the reset happens. Having a second, identical copy immediately following the first means the wrap-around lands on pixel-identical content, so the "reset" is invisible.
Yes — negate the SPEED constant, or flip the sign in the tick() function's pos += line, to reverse the automatic scroll direction; dragging is unaffected either way since it directly follows the cursor.
Continuous motion can be distracting or disorienting for some users — add a prefers-reduced-motion check that skips starting the requestAnimationFrame loop (leaving the ticker static but still draggable) for users who've requested reduced motion.