Skip to content

marquee

aa-marquee turns a list of items into a seamlessly looping horizontal marquee. The library measures the list, clones it as many times as needed to fill the viewport, then drives an infinite GSAP tween with a wrap modifier — no jump, no reset frame.

aa-marquee value tokens (space-separated, order-independent) control direction and interaction:

TokenEffect
(empty)Loop leftward (when aa-autoplay is present).
rightLoop rightward (timeScale = -1 on the same tween).
switchReverse direction while the page is scrolling up (driven by body[aa-scroll-direction]).
draggablePointer + touch drag modulates the loop; release settles to a steady cruise in the flick direction. With no aa-autoplay the drag simply settles (no cruise).
noneDon't init at this breakpoint. Combine with | (splits at md) or -sm / -md / -lg / -xl.

Add the aa-scrub attribute (separately) to layer a scroll-driven sweep on top of the loop — the row drifts left↔right as you scroll, while still cycling. See scrub below.

<!-- Looping marquee, reversed, pauses on hover -->
<div aa-marquee="right" aa-autoplay="30 hover-pause">
<div aa-marquee-track>
<div aa-marquee-list></div>
</div>
</div>
<!-- Static row on desktop, scrolling on mobile -->
<div aa-marquee aa-autoplay="none|40"></div>
AttributeDefaultNotes
aa-autoplay(static)Presence enables the loop; value = seconds per cycle (default 40). Tokens hover-pause / hover-slow. See Autoplay below.
aa-scrub(off)Presence enables the scroll-driven sweep. Value sets the ScrollTrigger scrub: true for an instant lock, or seconds for smoothing lag. Requires the [aa-marquee-scroller] wrapper.
aa-marquee-scroller20(on the scroller wrapper) Sweep travel as a percent of the marquee's own width: 30 = ±30% per side. Width-relative (not viewport-relative), so it feels the same regardless of browser width. Breakpoint-aware (|, -sm/-md/-lg/-xl).

The loop is driven by aa-autoplay, exactly like aa-slider and aa-tabs. Presence of the attribute enables the loop; without it the marquee is static (a scroll-driven sweep via aa-scrub can still apply). Its value is the seconds-per-cycle duration — how long the row takes to travel one full list-width. Lower = faster.

AttributeValueEffect
aa-autoplay(absent)Static — no auto-motion.
aa-autoplay(present, no value)Loop at the default 40s per cycle.
aa-autoplay"30"Loop at 30s per cycle.
aa-autoplay"30 hover-pause"Pause on mouseenter, resume on mouseleave. Skipped on touch and when draggable is set.
aa-autoplay"30 hover-slow"Ramp to 15% speed on hover, back on leave. Same gating; ignored when hover-pause is also set.
aa-autoplay"40|none"Loop on desktop, static on mobile (| splits at md; -sm/-md/-lg/-xl also work).

Required nesting for a basic marquee — two wrappers inside the viewport: [aa-marquee] > [aa-marquee-track] > [aa-marquee-list].

The [aa-marquee-scroller] wrapper is optional — add it (around the track) only when you want a scroll-driven sweep (aa-scrub). It's fine to keep it in your default markup, but a non-scrubbing marquee doesn't need it.

AttributeRole
aa-marqueeThe viewport. The lib's CSS applies overflow: hidden automatically; the attribute is also the ScrollTrigger target that gates the loop to on-screen visibility.
aa-marquee-scroller(optional, scrub only) The scroll-driven sweep layer, wrapping the track. Its value is the sweep travel as a percent of the marquee's own width (default 20). Omit entirely when not scrubbing.
aa-marquee-trackThe infinite-loop layer. The lib applies the looping translate here, and clones of [aa-marquee-list] (marked aa-marquee-clone) land here at runtime.
aa-marquee-listThe authored repeating unit. Items inside carry the spacing via margin. The lib measures scrollWidth here to compute the cycle distance.

The lib also sets aa-marquee-direction="left" \| "right" on the root while running — useful for CSS state hooks (e.g. flipping inner item rotation per direction).

The two transforms (scroller's scroll-driven sweep + track's infinite loop) sit on independent DOM layers, so the browser composes them additively — neither tween clobbers the other. With or without scrub, the markup shape stays the same; the scroller just becomes a no-op when scrub is absent.

Structural CSS ships in alrdy-animate.css: every wrapper gets display: flex; align-items: center, the inner three get flex: none, the viewport gets overflow: hidden, and the animated layers get will-change: transform. Authors only need to set width/height/spacing cosmetics — no flex-layout boilerplate required.

Use margin on each item to space items out. margin-right alone, or margin-left + margin-right — both work and produce uniform spacing across the wrap point. The lib measures [aa-marquee-list].scrollWidth (which folds margins into the cycle distance), so the seam between cloned lists matches the gap inside a list automatically. No JS configuration needed.

.marquee-item {
margin-right: 1rem; /* OK */
/* or */
margin: 0 0.5rem; /* also OK — left + right give equivalent spacing */
}

CSS gap / column-gap on the track is intentionally not supported — gap sits between siblings only and never contributes to the list's own width, so cloned lists would end up tighter at the seam than items are within a list. Stick to margin.

gsap, ScrollTrigger. Draggable + InertiaPlugin are additionally required when draggable is used (the same pair the slider needs, so most projects already load them).

  • aa-scrub layers on top of the loop — the track keeps cycling, plus a scroll-driven horizontal drift on the scroller layer. Combine freely with right (flips drift direction) and with no aa-autoplay (pure scroll-driven, no loop). The scroller and track sit on separate transform layers in markup so the two motions compose without one overriding the other.
  • draggable disables hover-pause, hover-slow, and switch for the same element — all of them would fight for control of the loop's progress / direction. Pick one interaction model per loop.
  • hover-pause and hover-slow (on aa-autoplay) are mutually exclusive; if both are set, hover-pause wins (slowing and pausing on the same pointerover would fight). hover-slow composes fine with switch — the ramp respects whichever direction is live when you hover.
  • No aa-autoplay means no loop: the marquee is static regardless of viewport entry, hover, or scroll direction. Scrub still works on top of it.
  • The whole loop pauses automatically when the marquee scrolls out of the viewport (top-bottom / bottom-top trigger), matching the slider/tabs pattern.

This basic loop omits the optional [aa-marquee-scroller] wrapper — just [aa-marquee] > [aa-marquee-track] > [aa-marquee-list].

Continuous
Loop
No
Reset
Frame
Ever

Live demo — scroll-driven only (no autoplay)

Section titled “Live demo — scroll-driven only (no autoplay)”

Two rows with no aa-autoplay — they never cruise on their own. Each has aa-scrub="1" and a 20% scroller, so the only motion is the scroll-driven sweep. The first drifts left, the second adds right to drift the opposite way. Scroll up and down past them to drive the sweep.

Scroll
Driven
Only
No
Autoplay
Here
Second
Row
Drifts
The
Other
Way

Live demo — right-direction with hover-pause

Section titled “Live demo — right-direction with hover-pause”

aa-marquee="right" reverses the loop; aa-autoplay="22 hover-pause" stops it under the cursor. Mouseleave resumes.

Hover
Me
To
Pause
Then
Release

aa-marquee="switch" with aa-autoplay="10 hover-slow" combines both interactions: the loop flips direction while the page scrolls up, and ramps down to 15% speed under the cursor. The two compose — hover slows whichever direction is currently live, so a mid-hover scroll-direction flip still reads. Scroll past it, then park the cursor over it.

Hover
To
Slow
Scroll
To
Switch

aa-marquee="switch" flips direction whenever the page is scrolling up. Scroll the page back and forth to see it react.

While running, the lib mirrors the live direction on the root as aa-marquee-direction="left" | "right" — useful for purely-CSS reactions like the arrows below. Each item carries a static glyph, and one CSS rule rotates it 180deg whenever the root flips to right, with a transform transition smoothing the flip.

Scroll
Down
Then
Scroll
Back
Up
.aa-marquee-arrow {
display: inline-block;
margin-right: 0.4rem;
transition: transform 0.4s ease;
}
/* lib sets aa-marquee-direction on the root every time direction flips */
[aa-marquee-direction="right"] .aa-marquee-arrow {
transform: rotate(180deg);
}

Add the aa-scrub attribute (and the [aa-marquee-scroller] wrapper) to layer a scroll-driven left↔right drift on top of the continuous loop. As the marquee enters the bottom of the viewport (top bottom) and exits the top (bottom top), the row drifts by the travel authored on the scroller — aa-marquee-scroller="30" = ±30% of the marquee's own width per side (default 20 = ±20%). Because it scales with the marquee's width rather than the viewport, the sweep feels the same whether the marquee is narrow or full-bleed, and at any browser width. Pass a number to aa-scrub for smoothing lag. The loop keeps cycling underneath, so the row stays alive even when you stop scrolling. Add right to flip the drift direction; drop aa-autoplay to get pure scroll-driven motion with no loop.

The travel is breakpoint-aware — aa-marquee-scroller="20|10" sweeps 20% on desktop, 10% on mobile.

Scroll up and down past this demo to see both motions compose.

Sweep
With
Scroll
Position
Not
Time

aa-marquee="draggable" adds pointer + touch drag. Flick to push the loop faster in either direction; release and the loop settles back to a steady cruise in the direction of your last flick.

Drag
Me
Or
Flick
Hard

Live demo — lazy-loaded images (regression test)

Section titled “Live demo — lazy-loaded images (regression test)”

The items here are <img loading="lazy"> logos with no width/height attributes, so each item's real width is only known once the image loads. Because this section sits far down the page, the browser defers those loads until you scroll near it — which is after init() measured the list. A naive marquee would lock its loop to the stale (near-zero) pre-load width and visibly snap back mid-list once the images grow.

The library guards against this: a ResizeObserver on [aa-marquee-list] catches the size change when the images land (and on late web-font swaps, or responsive item resizing) and rebuilds the loop and clones against the new measurement, preserving the loop's progress so the correction is seamless. Scroll down to this demo and watch — it should settle into a clean, jump-free loop with no mid-list reset.

Put aa-animate and aa-stagger on the [aa-marquee-list] element — its child items become the staggered entrance targets, revealing in sequence when the marquee scrolls into view. The lib strips the animation attributes and any from-state off the runtime clones, so duplicated items render solid instead of staying hidden; only your authored originals animate.

Because the marquee gates and clones at init, give the entrance an early aa-scroll-start="top 100%" so it fires the moment the row enters the viewport (before the loop has carried the originals off-screen).

Reveal
Down
Staggered
As
The
Marquee
Scrolls
Into
View
One
By
One