GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Marquee Infinite Loop — No JS, Scroll-Driven

A css marquee infinite loop is a row of duplicated content — logos, a price ticker, a headline — that slides on repeat using only a CSS transform and a

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine below build in the order the technique actually grows. 01 is the bare formula every other item reuses. 02 turns it into the layout most sites actually ship. 03 and 04 add an interaction and a visual finish. 05 and 06 bend the same formula onto a new axis and a second track. 07 and 08 are the idea-form entries — a non-linear rhythm and a curved, three-dimensional wall — and 09 closes on the one pattern built entirely around what a browser setting removes rather than what CSS adds. Every loop below runs on a two-second cycle so the grid keeps moving inside a fixed, non-scrollable iframe. A real banner's css marquee speed is usually ten to twenty seconds per lap, which reads calmer than what's playing above.

01Seamless loop formula

How it works

Six chips, lettered A through F, sit in a flex track duplicated into exactly two copies, and the track only ever moves from translateX(0) to translateX(-50%). Each chip carries its own margin-right for the space between them, so the first six chips add up to exactly half the track's total width and the clone lands on the original's start position without a seam.

Where it fits

Every pattern below borrows this same skeleton.

Seamless loop formula
Open in a new tab

01 · Seamless loop formula — Measured area 8.889% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateX(-50%)콘텐츠 2벌@keyframes
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 12s linear infinite;
}

.marquee__chip {
  margin-right: 14px;
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

02Infinite logo marquee

Turning it into a css logo marquee

Five wordmarks — NORTH, ACME, ORBIT, VELA, and KADE — sit inside pill-shaped cards under a small "Partners" label and stream through the same seamless track as item 01.

Where it fits

Nothing about the pattern changes; only the chip markup adds a shadow and a rounded background, so the row reads as a client strip instead of a raw alphabet test.

Infinite logo marquee
Open in a new tab

02 · Infinite logo marquee — Measured area 8.028% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

로고 카드gap 고정폭linear infinite
.marquee__track {
  display: flex;
  animation: logo-scroll 12s linear infinite;
}

.marquee__logo {
  margin-right: 16px;
  height: 34px;
  padding: 0 16px;
  border-radius: 999px;
  background: #fff;
  box-shadow: $shadow-press;
}

03Pause on hover

How it works

A single line — "Join today, first month free" — repeats once, and animation-play-state flips to paused the instant a pointer lands on :hover or a keyboard focuses the banner via :focus-within. That holds whatever word the reader was on.

Where it fits

Nothing about the track's position resets when it pauses, so hovering over the banner again a second later resumes from that same frame instead of restarting.

Pause on hover
Open in a new tab

03 · Pause on hover — Measured area 3.244% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

animation-play-statepausedhover
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track,
.marquee:active .marquee__track {
  animation-play-state: paused;
}

.marquee__track {
  display: flex;
  animation: marquee-scroll 10s linear infinite;
}

.marquee__chip {
  margin-right: 40px;
}

04Fade edges via mask

How it works

Chips numbered 01 through 05 stream through a track wrapped in a container carrying mask-image. That fades the two edges of the strip into whatever sits behind them instead of clipping them with a hard overflow: hidden line.

Where it fits

Unprefixed mask-image reached Chrome 120, Safari 15.4, and Firefox 53 — caniuse tracks the exact versions — so the -webkit- duplicate in the demo is mostly a habit at this point, not a real requirement for a modern target list.

Fade edges via mask
Open in a new tab

04 · Fade edges via mask — Measured area 10.501% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

mask-imagelinear-gradient-webkit-mask
.marquee {
  overflow: hidden;
  mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
  -webkit-mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
}

05Vertical ticker

How it works

Four rows — AAPL, GOOG, MSFT, NVDA, each with a price and an up or down arrow — sit in a fixed-height box and scroll upward instead of sideways. It uses translateY in place of translateX on the exact same two-copy, -50% formula from item 01.

Where it fits

Swapping the axis is the entire change; the duplication math underneath never moves.

Vertical ticker
Open in a new tab

05 · Vertical ticker — Measured area 9.767% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateY(-50%)column 트랙고정 높이
.ticker__track {
  display: flex;
  flex-direction: column;
  animation: ticker-scroll 12s linear infinite;
}

@keyframes ticker-scroll {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

06Dual direction rows

How it works

Two rows — DESIGN, MOTION, LAYOUT running forward and CSS, SCSS, REACT running backward — share one marquee-scroll animation, and only the second row adds animation-direction: reverse.

Where it fits

No second @keyframes block exists anywhere in the file; direction is the only property that differs between the two tracks.

Dual direction rows
Open in a new tab

06 · Dual direction rows — Measured area 14.222% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

animation-direction: reverse두 트랙교차 흐름
.marquee__track {
  animation-name: marquee-scroll;
  animation-duration: 12s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.marquee__row--rev .marquee__track {
  animation-direction: reverse;
}

07Variable-speed flow

How it works

The same -50% distance from item 01 runs under cubic-bezier(.65, 0, .35, 1) instead of linear, so the loop slows down right at the seam and speeds up through the middle of the lap.

Where it fits

Nothing about the duplication or the travel distance changes — only the rhythm a reader feels while the row is in motion.

Variable-speed flow
Open in a new tab

07 · Variable-speed flow — Measured area 5.417% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

cubic-bezier비선형 easing이음매 유지
.marquee__track {
  animation: marquee-scroll 9s
    cubic-bezier(.65, 0, .35, 1) infinite;
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

083D curved marquee

How it works

A perspective on the stage and preserve-3d on the track let five blank cards each carry their own rotateY and translateZ step, bowing the strip into a shallow cylinder instead of a flat plane. A Sass @for loop repeats the same five-step angle table across the cloned half of the track.

Where it fits

That way, the curve never flattens out or snaps at the seam the way a one-off, hand-written set of angles would.

3D curved marquee
Open in a new tab

08 · 3D curved marquee — Measured area 13.333% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

perspectivepreserve-3d단계별 rotateY
.stage { perspective: 420px; }

.marquee__track {
  display: flex;
  transform-style: preserve-3d;
  animation: marquee-scroll 12s linear infinite;
}

@for $i from 1 through 10 {
  $step: ($i - 1) % 5;
  .marquee__card:nth-child(#{$i}) {
    transform: rotateY(#{($step - 2) * 22}deg);
  }
}

09Static fallback for reduced motion

How it works

A toggle switches between the running track and the frozen, single-pass list that prefers-reduced-motion: reduce actually serves to a real visitor — News, Update, Event, no clones, no motion.

Where it fits

A status label flips between "Playing" and "Paused" alongside it, so the difference is something a reader can see rather than take on faith.

Static fallback for reduced motion
Open in a new tab

09 · Static fallback for reduced motion — Measured area 5.672% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

prefers-reduced-motion정적 목록토글 비교
@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none !important;
    transform: translateX(-30%);
  }
}

Where it breaks — the traps

Two problems in this set don't show up just by watching the loop play — one needs a screen reader to catch, the other needs a ruler.

The clone a screen reader reads twice

Item 02's duplicated wordmark row renders NORTH, ACME, ORBIT, VELA, and KADE on screen exactly once each, but the DOM holds every name twice so the seam can line up. A screen reader that isn't told to skip the clone announces all ten, turning five partners into what sounds like a company twice its real size. MDN's aria-hidden reference explains why nothing looks wrong on screen while this is happening: the attribute only removes an element from the accessibility tree; it never hides anything visually, so a sighted check of the page misses the bug completely. Marking every span in the cloned half with aria-hidden="true", the same pattern all nine demos use, fixes it without moving a single pixel.

The half-gap at the seam

The seamless-loop math itself hides a second problem that only shows up once you measure the seam instead of eyeballing it. Twelve chips split into two sets of six only create eleven gaps between them, not twelve, so putting the spacing on the track's gap property leaves translateX(-50%) a half-gap short of the clone's real position. Every track above that spaced its clones with gap — everything except item 05's vertical rows, which already touch with no gap between them — jumped at that seam on every restart, by an amount that measured 4px to 20px depending on the item's own spacing value. caniuse's flexbox gap table confirms every browser this site targets supports gap on flex containers, so the fix isn't a compatibility workaround.

The fix

It's margin-right on each individual chip instead of gap on the track: the spacing gets counted inside each set of six rather than between all twelve, so the first six chips land at exactly half the track's total width. Switching every affected demo above to that approach is exactly the fix bundled into the archive that unlocks with gcjydr57, typed in lowercase precisely as it reads in this sentence, digits and all.

Accessibility

Not the same stillness

All nine cancel their motion under prefers-reduced-motion: reduce, but they don't all land on the same kind of stillness. Items 01, 02, 04, 05, 06, 07, and 08 simply drop the animation and hold the track's last computed transform, so a reader sees a complete, unmoving row rather than a half-scrolled one. Item 03 needs no separate reduced-motion rule at all — the same :hover and :focus-within pause that helps a sighted mouse user also satisfies a vestibular one. Pausing on interaction already removes the motion a reduced-motion setting exists to prevent.

The one that shows the setting

Item 09 is the one built to demonstrate the setting rather than just obey it: flipping its toggle previews the exact frozen, single-pass list a real prefers-reduced-motion: reduce visitor gets, without needing to change an operating-system setting just to see it. MDN's page on the media feature covers which platform settings trigger it and how browsers read that preference.

Every other scroll-driven demo on this site sits behind the CSS category page, and what this project does and doesn't sell is laid out on the about page.

FAQ

Do I need JavaScript to build a css marquee infinite loop?

No — all nine tracks above move purely through a CSS @keyframes animation and a transform, with nothing reading scroll position or driving the loop from script. The zip in this article also ships a React version of each pattern for teams already working in components. The underlying animation in every one of them is still plain CSS, not a JS interval or a requestAnimationFrame loop.

Why does an infinite scrolling marquee css track jump at the seam?

The two most common causes are a duplicate count that isn't exactly double the original content, and a transform distance that isn't exactly -50% to match it. Either one leaves a visible gap or overlap once the loop restarts. A quieter version of the same stutter comes from gap on the flex container itself — the exact bug covered above. The count and the -50% distance can both be correct and the loop still jumps a few pixels at the seam, because gap only creates space between items: one fewer gap than the item count, not a trailing space after the last one in each set. Swapping gap on the track for margin-right on each item removes that jump.

Will a css marquee infinite loop pause automatically for users who prefer reduced motion?

Only if the CSS explicitly says so — nothing about animation: infinite respects a visitor's settings on its own. Each of the nine demos above carries its own @media (prefers-reduced-motion: reduce) block, or in item 03's case a pause trigger that already covers it. Leaving that block out means the track keeps scrolling regardless of what the reader's operating system requests.

Enter the archive password

The password is inside this article. You will find it as you read.