obsidian/wiki/concepts/css-marquee-animation-gpu-pattern.md
2026-05-10 21:26:56 +01:00

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.