---
title: "CSS Marquee — GPU-Composited Seamless Scroll Pattern"
source: daily/2026-05-10.md
updated: 2026-05-10
tags: [css, animation, performance, marquee, gallery]
---

# CSS Marquee — GPU-Composited Seamless Scroll Pattern

## Pattern

A seamless, stutter-free horizontal marquee (infinite scroll) entirely in CSS, GPU-composited via `transform: translateX()`:

```html
<div class="marquee-wrapper overflow-hidden">
  <div class="marquee-track">
    <!-- Items duplicated for seamless loop -->
    <div class="marquee-item">...</div>
    <div class="marquee-item">...</div>
    <!-- Duplicate set: -->
    <div class="marquee-item">...</div>
    <div class="marquee-item">...</div>
  </div>
</div>
```

```css
.marquee-track {
  display: flex;
  width: max-content;            /* shrink-wrap all items */
  animation: marquee 20s linear infinite;
  will-change: transform;        /* GPU layer hint */
}

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }  /* -50% = scroll exactly one set */
}
```

`-50%` works because the track contains **exactly 2 identical sets** of items — scrolling half the total width returns to the starting visual state, creating a seamless loop.

## Critical Rule: Never Mix CSS Animation With JS scrollLeft

**Do not manipulate `scrollLeft` or `scrollTop` on an element that has a CSS `animation` running.** The two control mechanisms conflict: the animation continuously overwrites the scroll position, causing:

- Frame drops / stuttering
- Animation appearing to "freeze" then jump
- Inconsistent behavior across browsers

If you need interactive controls (prev/next buttons), either:
- Use **pure JS** (remove CSS animation, manage position with `requestAnimationFrame`)
- Use **pure CSS** (keep the animation, disable prev/next buttons or hide them)

## `animation-play-state` Jump Bug

Toggling `animation-play-state: paused / running` causes a **visible position jump** when resuming. This happens because the animation timeline doesn't pause at the exact rendered position — the browser snaps to the nearest keyframe interpolation point on resume.

**Workaround:** Avoid pause/resume entirely. For hover-to-pause effects, prefer:

```css
.marquee-wrapper:hover .marquee-track {
  animation-play-state: paused;
}
```

This is acceptable for hover (fast re-trigger) but not for button-driven pause/play.

## Gallery Slide Pattern (Single Image at a Time)

When you want to show one image at a time (gallery mode, not continuous scroll), **don't use marquee**. Use:

- CSS `scroll-snap` with `overflow-x: scroll; scroll-snap-type: x mandatory`
- Or JS-driven `scrollTo({ left: targetX, behavior: 'smooth' })` with no CSS animation

Mixing marquee animation with gallery navigation (prev/next) is the #1 source of sticky/frozen animation bugs.
