← Back to gallery
CSS

Marquee / Ticker / Infinite Scroll

Seamless infinite-scroll ticker via duplicated tracks (aria-hidden on the duplicate) so the loop wraps without a visible seam. Three patterns: a continuous loop, a hover-pause ribbon, and a paired counter-direction layout.

marqueetickerinfinite-scrollduplicate-trackhover-pausearia-hiddenprefers-reduced-motion

Infinite ticker · duplicated rail

Marquee Ticker Infinite Scroll

Three motion archetypes for an authored CSS marquee — a continuous loop, a hover-paused reading rail, and a counter-direction pair — all built with duplicated tracks, white-space: nowrap, overflow: hidden viewport clipping, and translateX() keyframes that travel exactly one authored track width.

Always-on rail · status chips

Continuous Loop

A single rail of compact status pills slides right-to-left at a steady pace, never pausing — the canonical dashboard / release-feed marquee. Two duplicated copies of the chip list mean the loop offset of -50% lands exactly on a seam, so the wrap is invisible.

  • always-on
  • release feed
  • dashboard rail

Reading rail · pauses on hover

Hover-Pause Ribbon

A single ribbon of editorial headlines drifts at a slower readable cadence and stops the moment the pointer (or keyboard focus) lands on it, so visitors can read a passing story without chasing it. Resumes on pointer-leave / focus-out.

  • hover-pause
  • reading rail
  • editorial ribbon

Two rails · opposite directions

Reverse Pair

Two stacked rails of brand-logo placeholders travel in opposite directions — the top rail drifts left, the bottom rail drifts right — building a parallax-like sense of depth from a single keyframe. The classic landing-page "trusted by" wall.

  • parallax pair
  • brand wall
  • opposite directions

Marquee inspector

Continuous Loop

  • always-on
  • release feed
  • dashboard rail

A single rail of compact status pills slides right-to-left at a steady pace, never pausing — the canonical dashboard / release-feed marquee. Two duplicated copies of the chip list mean the loop offset of -50% lands exactly on a seam, so the wrap is invisible.

Helped you ship something? 🐟 Send my cat a churu

/* Single rail of duplicated chips travels right-to-left at constant speed; never pauses. */
.marquee-viewport {
  --marquee-duration: 14.00s;
  --marquee-gap: 1.00rem;
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.marquee-track {
  display: inline-flex;
  gap: var(--marquee-gap);
  white-space: nowrap;
  animation: marqueeDrift var(--marquee-duration) linear infinite;
  width: max-content;
}

@keyframes marqueeDrift {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-50% - (var(--marquee-gap) / 2))); }
}

@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; flex-wrap: wrap; }
}

How to make this

A seamless CSS marquee duplicates the visual items, clips them in an overflow viewport, and translates exactly one authored copy width.

html
1<section class="marquee-recipe" aria-label="Release updates">2  <p class="marquee-recipe__summary">    Ship notes live, beta seats open, docs synced, static export ready.  </p>5  <div class="marquee-recipe__viewport" aria-hidden="true">6    <div class="marquee-recipe__track">      <span>ship notes live</span>      <span>beta seats open</span>      <span>docs synced</span>      <span>static export ready</span>      <span>ship notes live</span>      <span>beta seats open</span>      <span>docs synced</span>      <span>static export ready</span>    </div>  </div></section> <style>.marquee-recipe {  width: min(100%, 32rem);  padding: 1rem;  color: #e2e8f0;  background: #0f1a2d;  border-radius: 16px;}.marquee-recipe__summary {  position: absolute;  inline-size: 1px;  block-size: 1px;  overflow: hidden;  clip: rect(0 0 0 0);}34.marquee-recipe__viewport {  overflow: hidden;  border-radius: 999px;  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);  mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);}.marquee-recipe__track {41  display: inline-flex;  gap: 1rem;  width: max-content;  white-space: nowrap;  animation: marquee-ticker-recipe-loop 14s linear infinite;}.marquee-recipe__track span {  padding: .45rem .75rem;  border-radius: 999px;  background: rgba(255, 255, 255, .07);  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .08);}@keyframes marquee-ticker-recipe-loop {  from { transform: translateX(0); }55  to { transform: translateX(calc(-50% - .5rem)); }}.marquee-recipe__viewport:hover .marquee-recipe__track {  animation-play-state: paused;}60@media (prefers-reduced-motion: reduce) {  .marquee-recipe__track {    animation: none;    flex-wrap: wrap;    width: 100%;    justify-content: center;  }}</style>

Annotated snippet

  1. Line 1The section exposes one accessible label for the whole ticker. The moving rail is not the only place where the updates exist.
    PitfallShould duplicated marquee content be aria-hidden?

    Yes, if the duplicated rail is decorative. Put the real update text once in normal or visually hidden content, then mark the looped visual copy aria-hidden so screen readers do not hear the same item twice.

  2. Line 2The readable summary appears once and is visually hidden. Assistive technology should not read duplicated loop content as repeated announcements.
    PitfallShould duplicated marquee content be aria-hidden?

    Yes, if the duplicated rail is decorative. Put the real update text once in normal or visually hidden content, then mark the looped visual copy aria-hidden so screen readers do not hear the same item twice.

  3. Line 5The viewport is aria-hidden because it contains two visual copies of the same items. Keep links or real announcements outside this decorative rail.
    PitfallShould duplicated marquee content be aria-hidden?

    Yes, if the duplicated rail is decorative. Put the real update text once in normal or visually hidden content, then mark the looped visual copy aria-hidden so screen readers do not hear the same item twice.

  4. Line 6The track contains two identical item sequences. Moving by half the track width lands exactly on the boundary between copy one and copy two.

    Both rails loop in 4s. The single-copy track shifts the entire list off-screen and snaps back to empty — a visible jump every cycle. The duplicated track uses margin-right (not flex gap) to keep each chip block the same width, so translateX(-50%) lands EXACTLY on the start of copy 2 and the loop is invisible.

    PitfallWhy does my marquee jump at the loop point?

    The reset is not landing on an identical frame. Duplicate the item sequence and translate exactly one copy width. If there is a gap between copies, include half that gap in the -50% travel or pad the track consistently.

  5. Line 34overflow clips the moving rail, and the mask fades the edges so items enter and leave without hard cut lines.

    Same -50% duplicated loop on both. Without a mask, chips hard-clip at the viewport border — they pop in and out as a sharp line. The mask-image linear-gradient fades the first and last 12% so chips dissolve into the edges instead.

    PitfallWhich browsers support the fade mask?

    Modern browsers support CSS masks, but Safari still commonly needs -webkit-mask-image. If the mask is unsupported, the rail should still work with hard clipped edges.

  6. Line 41inline-flex plus width: max-content makes the track measure to its content instead of stretching to the viewport.
  7. Line 55The keyframe travels -50% plus half the gap because the gap between the two copies is part of the visible seam distance.
    PitfallWhy does my marquee jump at the loop point?

    The reset is not landing on an identical frame. Duplicate the item sequence and translate exactly one copy width. If there is a gap between copies, include half that gap in the -50% travel or pad the track consistently.

  8. Line 60Reduced motion removes the loop and turns the duplicated rail into a static wrapped chip list in the same viewport.
    PitfallWhat should reduced motion do for the rail?

    Stop the loop and let the duplicated items wrap into a static chip list. Information should remain visible without movement.

Other pitfalls

How fast should a ticker move?
Slow enough that labels can be read without chasing them. Status chips can move faster than editorial headlines. For interactive or reading-heavy rails, provide pause on hover or focus and a static reduced-motion state.
Are CSS marquees expensive?
A single transform-only track is usually cheap. Long rails with many shadows, masks, images, or multiple opposite-direction rows can add paint cost, so keep the DOM shallow and test low-end mobile.

Notes

Overview

A CSS marquee ticker creates seamless infinite scroll via duplicated content tracks (aria-hidden on the duplicate). The track animates translateX from 0 to -50% on infinite loop; at -50% the view shows the start of the duplicate copy, identical to 0%, so the seam is invisible.

When to use it

Reach for marquees on partner-logo bars, social-proof ribbons, stock tickers, news headline strips. Skip them for body content where users need to read the text — scrolling that the user cannot pause is hostile. Always respect prefers-reduced-motion and offer a pause affordance for live tickers.

How it works

Render the content twice inside a single track wrapper (the second copy with aria-hidden="true" so screen readers do not announce duplicates). Set display: flex; width: max-content on the track and animate transform: translateX(0) → translateX(-50%) on linear infinite loop. At -50% the leftmost edge of the duplicate copy aligns exactly with what was the leftmost edge of the original at the start of the cycle — so the seam wraps without any visible jump. Use animation-duration computed from total track width to keep speed consistent across content widths.

This is the key difference between a production marquee and the obsolete <marquee> element: the DOM keeps one readable source track, the visual layer gets a duplicate track, and the duplicate is hidden from assistive tech. Searchers looking for an accessible marquee usually need that exact duplicated-track contract, not just a moving row of text.

Production gotchas

Forgetting width: max-content on the track causes the content to wrap or compress, breaking the seam. The duplicated content must be byte-identical — any difference (even a single random ID) breaks the visual loop. Pause-on-hover via :hover { animation-play-state: paused } is the friendly default for partner logos but the wrong default for live tickers where you don’t want accidental hovers to halt updates. Mobile Safari has historical bugs with sub-pixel rendering on long-running linear animations — if the marquee jitters every few seconds, add will-change: transform to force a dedicated compositor layer.

Accessibility

WCAG 2.2.2 requires that any auto-scrolling content users cannot pause be limited to 5 seconds — for marquees that exceed that, you must provide a pause control or honor prefers-reduced-motion. Under reduced motion the marquee should stop entirely and render a static snapshot. Mark the duplicate copy with aria-hidden="true" so screen readers do not announce content twice. Pause the animation on :focus-within as well so keyboard users reading content inside the track get a stable view.

References

Implementation depth

A seamless marquee depends on identical source and duplicate tracks. The track moves to -50%, where the duplicate begins at the same visual position the original had at 0%, so the loop can restart without a jump.

The duplicate must be hidden from assistive technology and the motion must be pausable. For live or long-running tickers, hover, focus-within, and reduced motion all need a stable readable state.