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

2.8 KiB

title source updated tags
CSS Marquee — GPU-Composited Seamless Scroll Pattern daily/2026-05-10.md 2026-05-10
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():

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

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.