--- 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
...
...
...
...
``` ```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.