Marquee
An accessible auto-scrolling marquee component for displaying scrolling content like logos, announcements, or featured items.
Features
- Smooth GPU-accelerated animations with seamless looping
 - Horizontal and vertical scrolling with RTL support
 - Pause on hover and keyboard focus
 - Customizable speed and spacing
 - Accessible and respects 
prefers-reduced-motion 
Installation
To use the marquee machine in your project, run the following command in your command line:
npm install @zag-js/marquee @zag-js/react # or yarn add @zag-js/marquee @zag-js/react
npm install @zag-js/marquee @zag-js/solid # or yarn add @zag-js/marquee @zag-js/solid
npm install @zag-js/marquee @zag-js/vue # or yarn add @zag-js/marquee @zag-js/vue
npm install @zag-js/marquee @zag-js/svelte # or yarn add @zag-js/marquee @zag-js/svelte
Anatomy
To set up the marquee correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the marquee package into your project
import * as marquee from "@zag-js/marquee"
The marquee package exports two key functions:
machine— The state machine logic for the marquee widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
You'll also need to provide a unique
idto theuseMachinehook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the marquee machine in your project 🔥
import * as marquee from "@zag-js/marquee" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" const logos = [ { name: "Microsoft", logo: "🏢" }, { name: "Apple", logo: "🍎" }, { name: "Google", logo: "🔍" }, { name: "Amazon", logo: "📦" }, ] function Marquee() { const service = useMachine(marquee.machine, { id: useId(), autoFill: true, }) const api = marquee.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> {/* Optional: Add fade gradient at start */} <div {...api.getEdgeProps({ side: "start" })} /> <div {...api.getViewportProps()}> {/* Render content (original + clones) */} {Array.from({ length: api.contentCount }).map((_, index) => ( <div key={index} {...api.getContentProps({ index })}> {logos.map((item, i) => ( <div key={i} {...api.getItemProps()}> <span className="logo">{item.logo}</span> <span className="name">{item.name}</span> </div> ))} </div> ))} </div> {/* Optional: Add fade gradient at end */} <div {...api.getEdgeProps({ side: "end" })} /> </div> ) }
import * as marquee from "@zag-js/marquee" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, For, Index } from "solid-js" const logos = [ { name: "Microsoft", logo: "🏢" }, { name: "Apple", logo: "🍎" }, { name: "Google", logo: "🔍" }, { name: "Amazon", logo: "📦" }, ] function Marquee() { const service = useMachine(marquee.machine, { id: createUniqueId(), autoFill: true, }) const api = createMemo(() => marquee.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> {/* Optional: Add fade gradient at start */} <div {...api().getEdgeProps({ side: "start" })} /> <div {...api().getViewportProps()}> {/* Render content (original + clones) */} <Index each={Array.from({ length: api().contentCount })}> {(_, index) => ( <div {...api().getContentProps({ index })}> <For each={logos}> {(item) => ( <div {...api().getItemProps()}> <span class="logo">{item.logo}</span> <span class="name">{item.name}</span> </div> )} </For> </div> )} </Index> </div> {/* Optional: Add fade gradient at end */} <div {...api().getEdgeProps({ side: "end" })} /> </div> ) }
<script setup> import * as marquee from "@zag-js/marquee" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed } from "vue" const logos = [ { name: "Microsoft", logo: "🏢" }, { name: "Apple", logo: "🍎" }, { name: "Google", logo: "🔍" }, { name: "Amazon", logo: "📦" }, ] const service = useMachine(marquee.machine, { id: "1", autoFill: true, }) const api = computed(() => marquee.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <!-- Optional: Add fade gradient at start --> <div v-bind="api.getEdgeProps({ side: 'start' })" /> <div v-bind="api.getViewportProps()"> <!-- Render content (original + clones) --> <div v-for="index in api.contentCount" :key="index - 1" v-bind="api.getContentProps({ index: index - 1 })" > <div v-for="(item, i) in logos" :key="i" v-bind="api.getItemProps()"> <span class="logo">{{ item.logo }}</span> <span class="name">{{ item.name }}</span> </div> </div> </div> <!-- Optional: Add fade gradient at end --> <div v-bind="api.getEdgeProps({ side: 'end' })" /> </div> </template>
<script lang="ts"> import * as marquee from "@zag-js/marquee" import { useMachine, normalizeProps } from "@zag-js/svelte" const logos = [ { name: "Microsoft", logo: "🏢" }, { name: "Apple", logo: "🍎" }, { name: "Google", logo: "🔍" }, { name: "Amazon", logo: "📦" }, ] const id = $props.id() const service = useMachine(marquee.machine, { id: id, autoFill: true, }) const api = $derived(marquee.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <!-- Optional: Add fade gradient at start --> <div {...api.getEdgeProps({ side: "start" })} /> <div {...api.getViewportProps()}> <!-- Render content (original + clones) --> {#each Array.from({ length: api.contentCount }) as _, index} <div {...api.getContentProps({ index })}> {#each logos as item} <div {...api.getItemProps()}> <span class="logo">{item.logo}</span> <span class="name">{item.name}</span> </div> {/each} </div> {/each} </div> <!-- Optional: Add fade gradient at end --> <div {...api.getEdgeProps({ side: "end" })} /> </div>
Auto-filling content
To automatically duplicate content to fill the container and prevent gaps during
animation, set the autoFill property in the machine's context to true.
const service = useMachine(marquee.machine, { autoFill: true, })
The api.contentCount property tells you the total number of content elements
to render (original + clones). Use this value in your loop:
{ Array.from({ length: api.contentCount }).map((_, index) => ( <div key={index} {...api.getContentProps({ index })}> {/* Your content */} </div> )) }
Note: The
api.multiplierproperty is also available if you need to know the duplication factor specifically (number of clones excluding the original).
Changing the scroll direction
To change the scroll direction, set the side property in the machine's context
to one of: "start", "end", "top", or "bottom".
const service = useMachine(marquee.machine, { side: "end", // scrolls from right to left in LTR })
Directional behavior:
"start"— Scrolls from inline-start to inline-end (respects RTL)"end"— Scrolls from inline-end to inline-start (respects RTL)"top"— Scrolls from bottom to top (vertical)"bottom"— Scrolls from top to bottom (vertical)
Adjusting animation speed
To control how fast the marquee scrolls, set the speed property in the
machine's context. The value is in pixels per second.
const service = useMachine(marquee.machine, { speed: 100, // 100 pixels per second })
Considerations:
- Higher values create faster scrolling
 - Lower values create slower, more readable scrolling
 - Speed is automatically adjusted based on content and container size
 
Setting spacing between items
To customize the gap between marquee items, set the spacing property in the
machine's context to a valid CSS unit.
const service = useMachine(marquee.machine, { spacing: "2rem", })
Reversing the animation direction
To reverse the animation direction without changing the scroll side, set the
reverse property in the machine's context to true.
const service = useMachine(marquee.machine, { reverse: true, })
Pausing on user interaction
To pause the marquee when the user hovers or focuses any element inside it, set
the pauseOnInteraction property in the machine's context to true.
const service = useMachine(marquee.machine, { pauseOnInteraction: true, })
This is especially important for accessibility when your marquee contains interactive elements like links or buttons.
Setting initial paused state
To start the marquee in a paused state, set the defaultPaused property in the
machine's context to true.
const service = useMachine(marquee.machine, { defaultPaused: true, })
Delaying the animation start
To add a delay before the animation starts, set the delay property in the
machine's context to a value in seconds.
const service = useMachine(marquee.machine, { delay: 2, // 2 second delay })
Limiting loop iterations
By default, the marquee loops infinitely. To limit the number of loops, set the
loopCount property in the machine's context.
const service = useMachine(marquee.machine, { loopCount: 3, // stops after 3 complete loops })
Setting
loopCountto0(default) creates an infinite loop.
Listening for loop completion
When the marquee completes a single loop iteration, the onLoopComplete
callback is invoked.
const service = useMachine(marquee.machine, { onLoopComplete() { console.log("Completed one loop") }, })
Listening for animation completion
When the marquee completes all loops and stops (only for finite loops), the
onComplete callback is invoked.
const service = useMachine(marquee.machine, { loopCount: 3, onComplete() { console.log("Marquee finished all loops") }, })
Controlling the marquee programmatically
The marquee API provides methods to control playback:
// Pause the marquee api.pause() // Resume the marquee api.resume() // Toggle pause state api.togglePause() // Restart the animation from the beginning api.restart()
Monitoring pause state changes
When the marquee pause state changes, the onPauseChange callback is invoked.
const service = useMachine(marquee.machine, { onPauseChange(details) { // details => { paused: boolean } console.log("Marquee is now:", details.paused ? "paused" : "playing") }, })
Adding fade gradients at edges
To add fade gradients at the edges of the marquee, use the getEdgeProps
method:
<div {...api.getRootProps()}> {/* Fade gradient at start */} <div {...api.getEdgeProps({ side: "start" })} /> <div {...api.getViewportProps()}>{/* Content */}</div> {/* Fade gradient at end */} <div {...api.getEdgeProps({ side: "end" })} /> </div>
Style the edge gradients using CSS:
[data-part="edge"][data-side="start"] { width: 100px; background: linear-gradient(to right, white, transparent); } [data-part="edge"][data-side="end"] { width: 100px; background: linear-gradient(to left, white, transparent); }
Styling guide
Required keyframe animations
For the marquee to work, you must include the required keyframe animations in your CSS. These animations control the scrolling behavior:
@keyframes marqueeX { 0% { transform: translateX(0%); } 100% { transform: translateX(var(--marquee-translate)); } } @keyframes marqueeY { 0% { transform: translateY(0%); } 100% { transform: translateY(var(--marquee-translate)); } }
Important: The animations use the --marquee-translate CSS variable which
is automatically set by the machine based on the side and dir props. This
enables seamless looping when combined with the cloned content.
Base content styles
To apply the animations, add these base styles to your content elements:
[data-scope="marquee"][data-part="content"] { animation-timing-function: linear; animation-duration: var(--marquee-duration); animation-delay: var(--marquee-delay); animation-iteration-count: var(--marquee-loop-count); } [data-part="content"][data-side="start"], [data-part="content"][data-side="end"] { animation-name: marqueeX; } [data-part="content"][data-side="top"], [data-part="content"][data-side="bottom"] { animation-name: marqueeY; } [data-part="content"][data-reverse] { animation-direction: reverse; } @media (prefers-reduced-motion: reduce) { [data-part="content"] { animation: none !important; } }
Note: The machine automatically handles layout styles (display,
flex-direction, flex-shrink) and performance optimizations
(backface-visibility, will-change, transform: translateZ(0)), so you only
need to add the animation properties.
CSS variables
The machine automatically sets these CSS variables:
--marquee-duration— Animation duration in seconds--marquee-spacing— Spacing between items--marquee-delay— Delay before animation starts--marquee-loop-count— Number of iterations (or "infinite")--marquee-translate— Transform value for animations
Styling parts
Earlier, we mentioned that each marquee part has a data-part attribute added
to them to select and style them in the DOM.
[data-scope="marquee"][data-part="root"] { /* styles for the root container */ } [data-scope="marquee"][data-part="viewport"] { /* styles for the viewport */ } [data-scope="marquee"][data-part="content"] { /* styles for each content container */ } [data-scope="marquee"][data-part="item"] { /* styles for individual marquee items */ } [data-scope="marquee"][data-part="edge"] { /* styles for fade edge gradients */ }
Orientation-specific styles
The marquee adds a data-orientation attribute for orientation-specific
styling:
[data-part="root"][data-orientation="horizontal"] { /* styles for horizontal marquee */ } [data-part="root"][data-orientation="vertical"] { /* styles for vertical marquee */ }
Paused state
When the marquee is paused, a data-paused attribute is set on the root:
[data-part="root"][data-paused] { /* styles for paused state */ }
Clone identification
Cloned content elements have a data-clone attribute for styling duplicates
differently:
[data-part="content"][data-clone] { /* styles for cloned content */ }
Side-specific styles
Each content element has a data-side attribute indicating the scroll
direction:
[data-part="content"][data-side="start"] { /* styles for content scrolling to inline-end */ } [data-part="content"][data-side="end"] { /* styles for content scrolling to inline-start */ } [data-part="content"][data-side="top"] { /* styles for content scrolling up */ } [data-part="content"][data-side="bottom"] { /* styles for content scrolling down */ }
Accessibility
ARIA attributes
The marquee component includes proper ARIA attributes:
role="region"witharia-roledescription="marquee"for proper semanticsaria-labeldescribing the marquee contentaria-hidden="true"on cloned content to prevent duplicate announcements
Keyboard interaction
When pauseOnInteraction is enabled:
- Focus — Pauses the marquee when any child element receives focus
 - Blur — Resumes the marquee when focus leaves the component
 
Motion preferences
The marquee automatically respects the user's motion preferences via the
prefers-reduced-motion media query, disabling animations when requested.
Best practices
- 
Use descriptive labels — Set a meaningful
aria-labelvia thetranslations.rootproperty:const service = useMachine(marquee.machine, { translations: { root: "Featured partner logos", // instead of generic "Marquee content" }, }) - 
Enable pause on interaction — Essential for accessibility when content contains links or important information:
const service = useMachine(marquee.machine, { pauseOnInteraction: true, }) - 
Consider infinite loops carefully — Infinite animations can cause discomfort for users with vestibular disorders. Consider providing pause controls or limiting loop iterations for critical content.
 - 
Decorative vs. informational — Marquees work best for decorative content (logos, testimonials). For important information, consider static displays or user-controlled carousels instead.
 
Methods and Properties
Machine Context
The marquee machine exposes the following context properties:
idsPartial<{ root: string; viewport: string; content: (index: number) => string; }>The ids of the elements in the marquee. Useful for composition.translationsIntlTranslationsThe localized messages to use.sideSideThe side/direction the marquee scrolls towards.speednumberThe speed of the marquee animation in pixels per second.delaynumberThe delay before the animation starts (in seconds).loopCountnumberThe number of times to loop the animation (0 = infinite).spacingstringThe spacing between marquee items.autoFillbooleanWhether to automatically duplicate content to fill the container.pauseOnInteractionbooleanWhether to pause the marquee on user interaction (hover, focus).reversebooleanWhether to reverse the animation direction.pausedbooleanWhether the marquee is paused.defaultPausedbooleanWhether the marquee is paused by default.onPauseChange(details: PauseStatusDetails) => voidFunction called when the pause status changes.onLoopComplete() => voidFunction called when the marquee completes one loop iteration.onComplete() => voidFunction called when the marquee completes all loops and stops. Only fires for finite loops (loopCount > 0).dir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The marquee api exposes the following methods:
pausedbooleanWhether the marquee is currently paused.orientation"horizontal" | "vertical"The current orientation of the marquee.sideSideThe current side/direction of the marquee.multipliernumberThe multiplier for auto-fill. Indicates how many times to duplicate content. When autoFill is enabled and content is smaller than container, this returns the number of additional copies needed. Otherwise returns 1.contentCountnumberThe total number of content elements to render (original + clones). Use this value when rendering your content in a loop.pauseVoidFunctionPause the marquee animation.resumeVoidFunctionResume the marquee animation.togglePauseVoidFunctionToggle the pause state.restartVoidFunctionRestart the marquee animation from the beginning.