Marquee
An endless scrolling strip for logos, tags, or testimonials, with a play/pause toggle.
Last updated
Uses JavaScript
Adds a play/pause toggle that pauses the CSS scroll animation. The loop itself is pure CSS — always animating — JS only adds the pause control.
Without JS: The play/pause button is hidden (no-js:hidden) — the strip keeps looping and can't be paused via the button. It still pauses on :focus-within and holds still for prefers-reduced-motion.
Show codeHide code
<Marquee label="Client logos">
<span>Larkspur</span>
<span>Fernhill</span>
<span>Brightwell</span>
<span>Halcyon</span>
</Marquee>Add
npx @ava-ui/apogee add marquee
No Apogee dependencies. Bring @lucide/astro for the play/pause toggle
icons. Ships one scoped JS island (progressive enhancement).
Usage
---
import Marquee from '@/components/apogee/Marquee/Marquee.astro'
---
<Marquee label="Client logos">
<img src="/logo-1.svg" alt="Larkspur" />
<img src="/logo-2.svg" alt="Fernhill" />
<img src="/logo-3.svg" alt="Brightwell" />
</Marquee>
The default slot is rendered once and duplicated into two looping panels internally — don’t wrap your items in extra markup expecting a single copy.
API
@composable — exposed to Content Editors in AVA. Any other attribute
passes through to the root ([key: string]: any).
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
'Marquee' |
Accessible label for the strip (aria-label on the role="group" root). |
direction |
'forwards' | 'backwards' |
'forwards' |
Scroll direction. backwards reverses the animation. |
speed |
number |
4 |
Seconds per item. Total loop duration scales with item count, so pace stays constant regardless of how many items you add. |
class |
string |
— | Extra classes on the root, merged last. |
Variants
Both directions:
Show codeHide code
<Marquee label="Forwards" direction="forwards">
<span>One</span>
<span>Two</span>
<span>Three</span>
</Marquee>
<Marquee label="Backwards" direction="backwards">
<span>One</span>
<span>Two</span>
<span>Three</span>
</Marquee>States
- Playing (default) — the track animates on a CSS loop; duration is
speed × item count. - Paused — click the toggle, or focus anything inside the strip
(
:focus-withinauto-pauses, so a keyboard user can’t lose a focused link off-screen). - Reduced motion — starts paused; still startable via the toggle.
- No JS — toggle button hidden; the strip keeps looping. See the JsNote above.
Examples
A slower strip of longer content:
Show codeHide code
<Marquee label="Headlines" speed={6}>
<span>Ship the release notes</span>
<span>Design review at 3pm</span>
<span>New docs are live</span>
</Marquee>Accessibility
role="group"witharia-label={label}on the root.- The duplicated panel (the seamless-loop copy) is
aria-hiddenandinert— never read or tabbed to twice. - The strip pauses on
:focus-within, so a keyboard user can read a focused link without it scrolling away. - Holds still for
prefers-reduced-motionby default; the toggle can still start it. - Keyboard —
Tabreaches items inside the strip (links, buttons) in document order, then the play/pause toggle;Enter/Spaceon the toggle switches play state. - No JS — the toggle is hidden (
no-js:hidden); the strip still loops via CSS and still pauses on focus and for reduced motion — only the manual pause control is unavailable.
Tokens
| Token | Used for |
|---|---|
--border-neutral |
Toggle button border |
--background-page |
Toggle button background |
--foreground-neutral |
Toggle button icon colour |
--background-neutral-faded-highlighted |
Toggle button hover background |
--outline-color-focus |
Toggle button focus ring |
Do / Don’t
- Do keep the item count reasonable — very short lists loop awkwardly even with pace-scaling.
- Do give images real
alttext, oralt=""if purely decorative. - Don’t put content someone must read carefully inside a Marquee — motion undermines reading; reserve it for decorative or ambient content.
- Don’t rely on the marquee alone for critical information — a no-JS or fast-scrolling user may not catch it.
Content
- Pass a
labeldescribing the strip’s purpose (“Client logos”, “Latest headlines”) — it’s announced once via the root’saria-label. - Keep items short — long strings force a longer loop and awkward wrapping.