76 lines
2.8 KiB
Markdown
76 lines
2.8 KiB
Markdown
---
|
|
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.
|