Infinite Marquee

A ticker that loops without a visible jump.

← All motions

Infinite Marquee

Every moving ticker looks the same in a screenshot and most of them have a visible stutter at the loop point. The seam is not a timing problem — it is a measurement problem. Two identical copies inside one track, translated by exactly half the track width, is the only version that has no join.

trigger: load transform: translateX duplicated track

Live demo

Editorial Brutalism Swiss Liquid Glass Bento Grid Retro Pixel Art Deco Acid Graphics

The prompt

Create a marquee — a horizontally scrolling row of items that loops with no visible join.

Structure:
- A viewport element with overflow: hidden.
- Inside it, one track element containing the same set of items TWICE, as two sibling groups. The second group must be a byte-identical copy of the first and must carry aria-hidden="true".
- The track is display: flex, width: max-content. Each group is display: flex with its own internal gap.
- Put the gap INSIDE each group, never between the groups. A margin between groups is what creates the seam.

Motion:
- Animate the track with transform: translateX(0) to translateX(-50%), linear, infinite, 20 to 40 seconds depending on content length.
- Because the track contains exactly two identical groups, -50% is exactly one group width. That is why the loop is seamless.
- Animate transform only. Animating left or margin-left forces layout on every frame.

Control:
- Pause on hover and on focus-within, using animation-play-state: paused. Any auto-moving content that runs longer than five seconds needs a way to stop.
- Under prefers-reduced-motion: reduce, remove the animation entirely and let the row scroll horizontally by hand instead of freezing it mid-word.

Constraints:
- The row must not trap focus or steal keyboard navigation; items are static text, not links, unless they are real links.
- Content must remain readable at the loop point — no half-visible words at reduced motion.
- Do not use this inside a form, near an input, or as the primary headline.

Fallback:
- Without JavaScript this works as-is, because the whole effect is CSS. Do not animate with requestAnimationFrame unless you need direction changes on drag.

What actually makes it work

Compatibility and performance

AspectDetail
transform animationUniversal. Runs on the compositor; no layout and no paint after the first frame.
animation-play-stateUniversal.
CostOne composited layer the width of the duplicated track. Long rows with images get expensive — keep the payload per item small.
MemoryThe track is twice the content width. Above roughly 4000px of traffic-strip content, consider a JS-driven approach that recycles nodes.

One practical note: the duplicate group is a real DOM cost, not a visual one. Screen readers would otherwise announce everything twice, which is why the second group carries aria-hidden="true". Skipping that attribute is the most common accessibility defect in this pattern.

When not to use it

Don't put a sentence in it. Anything a visitor has to read cannot be moving at a constant speed. A ticker is for names, logos, categories and tags.

Don't use it as the only place information appears. If the strip is the sole presentation of a category list, anyone who stops the animation or uses a screen reader loses half of it. Provide a static list elsewhere on the page.

Don't run it in a hero next to a CTA. Two things moving at once — a marquee and a pulsing button — competes for the same attention and neither wins.