2.8 KiB
| title | source | updated | tags | |||||
|---|---|---|---|---|---|---|---|---|
| CSS Marquee — GPU-Composited Seamless Scroll Pattern | daily/2026-05-10.md | 2026-05-10 |
|
CSS Marquee — GPU-Composited Seamless Scroll Pattern
Pattern
A seamless, stutter-free horizontal marquee (infinite scroll) entirely in CSS, GPU-composited via transform: translateX():
<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>
.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:
.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-snapwithoverflow-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.