Timeline

Seamless loop

A seamless loop is a GSAP animation set to `repeat: -1` whose end state matches its start state exactly, so it plays forever with no visible jump at the loop point. It's the core technique behind marquees, logo ribbons, tickers, and any motion that needs to run continuously without the eye ever catching the seam.

Updated June 30, 2026

Mechanics

How a seamless loop works

repeat: -1 tells a tween or timeline to play again the moment it finishes, indefinitely. That alone gives you an infinite loop, but not a seamless one. The seam appears when frame 0 and the final frame don't line up: the playhead snaps back to the start and you see a jump.

The fix is to make the start and end states identical. For a horizontal marquee, you duplicate the content and animate the track by exactly the width of one copy. When the first copy has scrolled fully off, the duplicate is sitting in precisely the position the first copy started in, so the reset is invisible.

marquee-loop.js

ease: "none" (linear) is non-negotiable for a continuous loop. Any easing curve speeds up and slows down across each pass, so the motion visibly pulses at every repeat boundary. Constant velocity is what reads as 'one endless ribbon' instead of 'the same animation playing over and over'.

yoyo is not a loop

yoyo: true reverses direction on every repeat, so the content scrolls left, then right, then left. That's a ping-pong, not a marquee. Use yoyo for back-and-forth pulses (a breathing scale, a hover shimmer), never for content that should travel one direction forever.
Wrapping

The modifiers + wrap approach

Duplicating content covers most marquees. When you need many items looping independently, or drag control, or items that wrap individually rather than as one block, use a modifiers function with gsap.utils.wrap. The modifier rewrites each frame's value so anything that scrolls past the end is mapped back to the start, keeping every element inside a fixed range.

wrap-modifier.js

This is the engine inside GSAP's well-known horizontalLoop helper: a repeat: -1 timeline of per-item tweens, with modifiers: { time: gsap.utils.wrap(0, duration) } so the playhead itself wraps. It's heavier than the duplicate-and-translate trick, so reach for it only when you actually need per-item control or scrubbing.

When

Use a seamless loop for

  • Horizontal marquees and partner-logo ribbons
  • Text tickers and announcement bars
  • Vertical testimonial feeds that scroll on their own
  • Continuous shimmer or gradient sweeps across text
  • Any decorative motion that should run as long as the section is on screen
Alternatives

Use something else when

  • The motion is a single back-and-forth pulse, use repeat: -1 with yoyo: true instead of matched start/end states
  • The loop should react to scroll speed rather than run at a fixed rate, that's scroll-velocity (see the Velocity Type component), not a timeline loop
  • You only need a finite number of passes, set repeat: 3 and skip the seam-matching entirely
  • It's a purely cosmetic CSS-only marquee with no drag or pause logic, a CSS @keyframes translate loop is lighter than loading GSAP for that one job
In production

Used in these Annnimate components

  • The Infinite Marquee runs a repeat: -1 timeline with ease: "none", clones the content to fill the track, and adds gsap.utils.wrap modifiers so dragging and momentum stay inside the loop
  • The Text Shimmer Wave uses repeat: -1 with a small repeatDelay to sweep a highlight across the characters on a constant cadence

Keep the loop composited

A loop runs every frame the section is visible, so the cost of an un-optimized one is paid continuously. Non-composited animations show up on 40% of mobile pages (HTTP Archive Web Almanac 2025, https://almanac.httparchive.org/en/2025/performance). Animate xPercent / x (transform-only, GPU-composited), never left or margin, or a constantly-running marquee becomes the jank on the page.
Used in components

See it running in production

FAQ

Common questions

How do I make an infinite marquee in GSAP?
Duplicate your content so the track holds two identical copies side by side, then tween the track to xPercent: -50 with duration set to taste, ease: "none", and repeat: -1. Because the second copy is sitting exactly where the first started, the reset at the end of each pass is invisible. That's the whole trick: matched start/end states plus a linear ease.
Why does my marquee jump or stutter at the loop point?
Two usual causes. Either your end state doesn't match the start state (you animated by the wrong distance, so frame 0 and the last frame differ and the snap-back is visible), or you used an ease other than none (the speed changes across each pass, so the boundary pulses). Move by exactly one copy's width and set ease: "none".
What's the difference between repeat: -1 and yoyo for a loop?
repeat: -1 replays the same animation from the start every time, which is what a marquee needs (content keeps travelling one direction). yoyo: true makes each repeat run in reverse, so the content scrolls one way then back, a ping-pong. Use yoyo for a pulsing or breathing effect, not for a ticker or ribbon.
How do I pause a GSAP loop on hover?
Keep a reference to the tween or timeline and call loop.pause() on mouseenter and loop.play() on mouseleave. For a softer feel, animate the timeScale instead: gsap.to(loop, { timeScale: 0, duration: 0.4 }) on enter and back to 1 on leave, so the marquee eases to a stop rather than freezing mid-motion.
Should I use a CSS animation or GSAP for a marquee?
For a purely decorative, always-on scroll with no interaction, a CSS @keyframes translate loop is lighter and needs no JS. Reach for GSAP when you want drag-to-scrub, momentum, pause-on-hover, direction changes, or per-item wrapping, the kind of control the Infinite Marquee component ships. The seam-matching principle (matched start/end, linear timing) is identical in both.