Grab-to-Pause Auto-Scroll Ticker — HTML CSS JS Snippet

Grab-to-Pause Auto-Scroll Ticker · Carousels · Plain HTML, CSS & JS · Live preview

What's included

Features

Seamless infinite auto-scroll from a finite chip set, via a single duplicated copy and a wrapped position
One shared position variable, written by either the autoplay loop or the active drag — never both at once
Grabbing mid-scroll takes over instantly; releasing resumes autoplay from precisely that same position
Correct negative-safe modulo wrapping, so dragging backward past the start still loops cleanly
Unified pointer and touch handling for the grab-and-drag gesture
Loop width recalculated on resize, keeping the seamless wrap accurate at any container size

About this UI Snippet

Grab-to-Pause Auto-Scroll Ticker — One Position Variable, Two Different Writers

Screenshot of the Grab-to-Pause Auto-Scroll Ticker snippet rendered live

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

  1. 1
    Paste HTML, CSS, and JSA row of technology chips scrolls continuously and smoothly to the left, looping seamlessly forever.
  2. 2
    Click and hold anywhere on the tickerAuto-scrolling stops instantly and the ticker follows your cursor precisely.
  3. 3
    Drag left or rightScrub through the chips manually in either direction, well beyond the normal auto-scroll range.
  4. 4
    ReleaseAuto-scrolling resumes immediately from exactly wherever you released it — no jump or reset.
  5. 5
    Add 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

Technology or client logo tickers
A classic "as seen in" or tech-stack strip that a curious visitor can also grab and browse manually.
Skill or tag showcases on a portfolio
An always-moving accent that remains genuinely interactive rather than purely decorative.
Announcement or promo tickers
Continuously scroll several short messages while still letting a user pause and read one manually.
Category or filter chip rails
An eye-catching idle animation for a chip row that becomes a normal draggable list the moment it's touched.

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how cloning the chip set once and wrapping the position with a double-modulo formula produces a seamless infinite loop from a finite number of elements, and why a plain single modulo (%) wouldn't correctly handle a negative drag position in JavaScript. It's also worth asking the assistant to add a prefers-reduced-motion check that keeps the ticker static (but still draggable) for users who've requested reduced motion, or to add momentum so releasing after a fast drag continues scrolling briefly before settling back into the steady autoplay speed.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a continuously auto-scrolling, infinitely looping ticker of chip elements in plain HTML, CSS, and vanilla JavaScript that can be grabbed and dragged at any moment to take manual control, resuming automatic scrolling from the exact released position — no library.

Requirements:
- A horizontal row of chip elements, where the full set of original chips is duplicated once (cloned and appended immediately after the originals) so the track contains two identical, back-to-back copies of the content — this is what enables a seamless infinite loop from a finite number of elements.
- A single shared numeric position variable representing horizontal scroll offset, which is wrapped into the range from zero up to (but not including) one full copy's total width using a correctly negative-safe modulo calculation, so the position can grow or shrink without bound while the visually applied offset always stays within one seamless loop cycle.
- A requestAnimationFrame loop that, using real elapsed time between frames (not a fixed per-frame increment), continuously advances the position variable at a constant pixel-per-millisecond rate whenever the ticker is not currently being dragged, and applies the wrapped position as a CSS transform on every frame.
- Pointer-based (with touch fallback) drag handling: on pointerdown, record the starting cursor position and the position variable's current value; while dragging, overwrite the position variable directly based on the cursor's movement delta from that starting point, completely overriding the automatic advancement for the duration of the drag; on release, simply stop overriding it, allowing the requestAnimationFrame loop to resume automatically advancing from whatever value the position variable was left at.
- The total width of one copy of the content must be measured from the real rendered widths of the original (non-cloned) elements, recalculated on window resize, not assumed or hardcoded.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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>