Skip to main content

Marquee

An accessible auto-scrolling marquee component for displaying scrolling content like logos, announcements, or featured items.

Apple
Banana
Cherry
Grape
Watermelon
Strawberry
Properties

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/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-part attribute to help identify them in the DOM.

No anatomy available for marquee

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 id to the useMachine hook. 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 🔥

<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.multiplier property 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 loopCount to 0 (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" with aria-roledescription="marquee" for proper semantics
  • aria-label describing the marquee content
  • aria-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

  1. Use descriptive labels — Set a meaningful aria-label via the translations.root property:

    const service = useMachine(marquee.machine, { translations: { root: "Featured partner logos", // instead of generic "Marquee content" }, })
  2. Enable pause on interaction — Essential for accessibility when content contains links or important information:

    const service = useMachine(marquee.machine, { pauseOnInteraction: true, })
  3. 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.

  4. 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.

Data Attributes

Root
data-scope
marquee
data-part
root
data-state
"paused" | "idle"
data-orientation
The orientation of the marquee
data-paused
Present when paused
Viewport
data-scope
marquee
data-orientation
The orientation of the viewport
Content
data-scope
marquee
data-index
The index of the item
data-orientation
The orientation of the content
Edge
data-scope
marquee
data-orientation
The orientation of the edge

CSS Variables

Root
--marquee-duration
The marquee duration value for the Root
--marquee-spacing
The marquee spacing value for the Root
--marquee-delay
The marquee delay value for the Root
--marquee-loop-count
The marquee loop count value for the Root
--marquee-translate
The marquee translate value for the Root
Edit this page on GitHub