← All articles
Framer MotionCSSAnimation

A Framer Motion marquee that never stalls (use CSS keyframes)

Why JS-driven marquees stutter on real pages, and how a pure CSS keyframe loop fixes it for good. With a copy-paste component.

21 May 2026 · 6 min read

A logo strip that scrolls forever looks trivial. Then you ship it, scroll past it on a real page, and watch it hitch. The motion goes smooth, smooth, smooth, then jumps a few pixels and carries on. On slower devices it can appear to stall outright for a fraction of a second. This is almost always the same bug, and it is a bug of architecture, not of tuning.

Why a JS-timeline marquee stutters

The instinct with Framer Motion is to animate x on a very wide element: take a track that holds your content, push its x from 0 to its negative width, loop, repeat. Framer Motion drives that animation from a requestAnimationFrame loop on the main thread. Every frame, JavaScript wakes up, computes the next x value, and writes it to the element.

That works fine in isolation. It falls apart on a real page because the main thread is shared. Anything else competing for it - a React re-render, an image decode, a third-party script, an IntersectionObserver callback, garbage collection - delays the next frame callback. When the callback runs late, the marquee either skips the frames it missed (a visible jump) or renders them all at once (a stutter). The animation is only as smooth as your busiest frame.

You can mitigate this. You cannot fix it. As long as the position is computed in JavaScript, the marquee is hostage to the event loop.

Why CSS keyframes do not have this problem

A CSS @keyframes animation on transformis handed to the browser's compositor once, up front. The compositor runs on its own thread. It does not need JavaScript to produce each frame - it already knows the start state, the end state, the duration, and the timing function, so it interpolates them itself.

The practical consequence: a busy main thread cannot stall a compositor animation. Your React tree can be mid-render, a script can be parsing, and the marquee keeps gliding because nothing on the main thread is in its critical path. transform and opacity are the two properties the compositor can animate without involving layout or paint, which is exactly why this works.

The seamless-wrap trick

The one detail that makes a marquee loop without a visible seam: put your content in the track twice, then translate the track by -50%. When the animation reaches the halfway point, the second copy sits exactly where the first copy started, so resetting to 0 is invisible. The track always looks full.

The copy-paste component

Here is the whole thing. The CSS:

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

.marquee {
  overflow: hidden;
}

.marquee__track {
  display: flex;
  width: max-content;
  gap: 3rem;
  animation: marquee 25s linear infinite;
}

/* Pause on hover - no JS needed */
.marquee:hover .marquee__track {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
}

And the React component that renders the content twice:

function Marquee({ children }: { children: React.ReactNode }) {
  return (
    <div className="marquee">
      <div className="marquee__track">
        <div className="marquee__group">{children}</div>
        {/* Second copy. aria-hidden so screen readers
            do not announce the content twice. */}
        <div className="marquee__group" aria-hidden="true">
          {children}
        </div>
      </div>
    </div>
  );
}

Two rules carry the whole effect. animation: marquee 25s linear infinite with a linear timing function keeps the speed perfectly constant - an ease would slow at the loop boundary and give the seam away. And animation-play-state: paused on hover gives you a tasteful pause-to-read interaction for free, with no event listeners and no state.

When you actually want JavaScript

CSS keyframes win when the motion is a fixed, looping translation. They cannot react to anything. If you want the marquee to respond to scroll velocity, drag, or pointer position, you do need JavaScript - and Framer Motion's useAnimationFrame with a value driven by useMotionValue is the right tool. But for the common case - a logo wall, a testimonial strip, a word backdrop that simply needs to drift forever - reach for the keyframe loop. It is fewer lines, no dependency, and it physically cannot be stalled by your app.

The logo marquee, hero marquee, and testimonial marquee blocks all use this exact compositor-driven approach, with the duplicate-track wrap and the reduced-motion fallback already wired in.

Components in this article

Want the whole pack?

Every component in this article ships in shadcn Motion Blocks - one file each, tunable in a live studio. €19 for all of them.

See pricing