CoreReveals and Continuous Motionno dependencies

InfiniteMarquee

Seamless looping ticker marquee supporting configurable direction, speed, gap spacing, pause-on-hover, and aria-hidden track duplication without layout jumps.

Preview

KineticTypeGlyphCascadeLiquidGlassCardHoloTiltCardMagneticDockMorphingCommandPalette

Customize

4 props
22
14

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add infinite-marquee

Writes components/ui/infinite-marquee.tsx and components/styles/infinite-marquee.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { InfiniteMarquee } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <InfiniteMarquee />6  );7}

Props

PropTypeDefault
items

Array of labels or badges to scroll seamlessly.

string[]['KineticType', ...]
direction

Horizontal travel direction.

'left' | 'right''left'
speed

Duration in seconds for one complete loop.

number22
gap

Gap in pixels between consecutive items.

number14
pauseOnHover

Pauses the marquee when hovered or focused.

booleantrue

Accessibility

Marks the duplicated second track aria-hidden='true' so screen readers only read the item list once, and pauses translation automatically on hover, focus-within, or prefers-reduced-motion.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/infinite-marquee.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/infinite-marquee.module.css";5 6export interface InfiniteMarqueeProps {7  /** String items to display in the marquee */8  items?: string[];9  /** Scroll direction */10  direction?: "left" | "right";11  /** Loop cycle duration in seconds */12  speed?: number;13  /** Gap between items in pixels */14  gap?: number;15  /** Pause animation on pointer hover */16  pauseOnHover?: boolean;17  /** Optional className */18  className?: string;19}20 21const DEFAULT_ITEMS = [22  "KineticType",23  "GlyphCascade",24  "LiquidGlassCard",25  "HoloTiltCard",26  "MagneticDock",27  "MorphingCommandPalette",28];29 30export function InfiniteMarquee({31  items = DEFAULT_ITEMS,32  direction = "left",33  speed = 22,34  gap = 14,35  pauseOnHover = true,36  className = "",37}: InfiniteMarqueeProps) {38  const safeItems = items.length > 0 ? items : DEFAULT_ITEMS;39  const styleVars = {40    "--mf-mq-speed": `${speed}s`,41    "--mf-mq-gap": `${gap}px`,42    "--mf-mq-dir": direction === "right" ? "reverse" : "normal",43  } as React.CSSProperties;44 45  return (46    <div47      style={styleVars}48      className={`${styles.viewport} ${pauseOnHover ? styles.pauseOnHover : ""} ${className}`.trim()}49    >50      <div className={styles.track}>51        {safeItems.map((item, idx) => (52          <span key={`primary-${idx}`} className={styles.pill}>53            {item}54          </span>55        ))}56      </div>57      <div aria-hidden="true" className={styles.track}>58        {safeItems.map((item, idx) => (59          <span key={`clone-${idx}`} className={styles.pill}>60            {item}61          </span>62        ))}63      </div>64    </div>65  );66}