The marquee is one of the most loved components by designers and one of the most hated by anyone who has to build it properly. This lesson builds a real one from a blank canvas — not just a marquee that scrolls, but a system: seamless in any direction, reusable across the whole site with a single attribute, and solid enough to trust in production.
More than the result, it’s the reasoning that matters here. Every “why” — why two copies, why -100%, where overflow goes, why an attribute — is what turns a copied snippet into a technique you own.
How it works
Two trail groups make the loop seamless. A single row of items can’t loop cleanly — when it scrolls out there’s nothing behind it, so you get a gap and a jarring reset. The fix is to duplicate the whole content into two identical groups (each a div with the class section_marquee-trail) sitting side by side inside a flex section_marquee. Animate the groups leftward, and just as the first slides fully out, the second is exactly where the first began — reset instantly and it reads as an infinite strip.
Animate by -100%, not a measured width. The naive approach animates the container by a computed amount (item width × count). That’s fragile: it breaks the moment items have different widths or you add one. Because both trail groups have identical width, animating them by -100% of their own width always lands the second group where the first started — no matter how many items or what sizes. That’s the general solution. (Duration ~40s, easing linear so the scroll is perfectly constant. Set the trail groups’ flex sizing to don’t shrink or grow so they take their natural width.)
Overflow on the container, never the body. To hide the off-screen content, set overflow: hidden on the main container through a combo class (is-marquee) rather than the base class — that keeps the visible marquee bounded to your content max-width, which usually looks best, without touching the container everywhere else. Setting overflow hidden on the body is a hard no.
One attribute makes it reusable. Rather than rebuild the interaction per marquee, give each marquee wrapper a data-animate attribute (e.g. horizontal-marquee) and target the action with any element → direct child of → attribute. That animates the two trail groups of any marquee carrying the attribute, independent of its class names — so one interaction powers them all (leave the “first match only” box unchecked, since there are two children).
Direction is just axis + sign. From this base every variant is a small change: horizontal animates move X; vertical animates move Y; diagonal animates both. Reverse any of them by animating from -100% back to 0 instead of 0 to -100%. Pausing on hover is a separate hover interaction (control pause on mouse-enter, resume on mouse-leave), and you can layer a CSS hover on the items themselves for extra life.
How to use it
-
Structure. Section → main container (with a max-width) →
section_marquee(display flex). Inside, wrap all your items in asection_marquee-traildiv (flex), then duplicate that trail so there are two identical groups. Set the trail groups’ flex sizing to don’t shrink or grow; add a small right margin to items for spacing. -
Clip it. Add an
is-marqueecombo class to the main container withoverflow: hiddenand zero horizontal padding. -
Core interaction. Page-load GSAP interaction. Give the
section_marqueewrapperdata-animate="horizontal-marquee". Action: target any element → direct child of →data-animate=horizontal-marquee, animate move X to -100%, duration ~40s, linear, repeat: infinite. -
Variants. Duplicate the section, change the attribute value (e.g.
reversed-horizontal-marquee) and the action’s target to match. Reverse = animate move X from-100%to0. Vertical = animate move Y. Diagonal = animate X and Y together. Reversed diagonal = flip both. -
Pause on hover. Add a hover interaction on the marquee: mouse-enter → control pause, mouse-leave → control resume.