# Carousel Marquee Infinite — React Carousel Component

> An endless row that glides in either direction and eases to a stop under the pointer instead of freezing, then eases back up to speed.

- Category: [Carousels](https://pixelpapa.com/components/carousels.md)
- Tags: carousel, marquee, infinite, loop, hover
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/carousels/carousel-marquee-infinite

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/carousel-marquee-infinite.json"
```

The file is written to `components/pixelpapa/carousel-marquee-infinite.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Star } from "lucide-react";
import { motion, useAnimationFrame, useMotionValue, useReducedMotion } from "motion/react";
import { useRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type MarqueeProps = {
  children: ReactNode;
  direction?: "left" | "right";
  /** Pixels per second. */
  speed?: number;
  pauseOnHover?: boolean;
  gap?: number;
  className?: string;
};

export function Marquee({ children, direction = "left", speed = 36, pauseOnHover = true, gap = 16, className }: MarqueeProps) {
  const group = useRef<HTMLDivElement>(null);
  const x = useMotionValue(0);
  const factor = useRef(1);
  const target = useRef(1);
  const reduce = useReducedMotion();

  useAnimationFrame((_, delta) => {
    const w = group.current?.offsetWidth;
    if (!w || reduce) return;
    const dt = Math.min(delta, 50);
    // Ease the speed toward its target so pausing feels like braking.
    factor.current += (target.current - factor.current) * Math.min(1, dt / 220);
    const span = w + gap;
    let next = x.get() + (direction === "left" ? -1 : 1) * speed * factor.current * (dt / 1000);
    if (next <= -span) next += span;
    if (next > 0) next -= span;
    x.set(next);
  });

  const pause = (v: boolean) => {
    if (pauseOnHover) target.current = v ? 0 : 1;
  };

  return (
    <div
      onPointerEnter={() => pause(true)}
      onPointerLeave={() => pause(false)}
      onFocus={() => pause(true)}
      onBlur={() => pause(false)}
      className={cn("overflow-hidden [mask-image:linear-gradient(90deg,transparent,black_10%,black_90%,transparent)]", className)}
    >
      <motion.div style={{ x, gap }} className="flex w-max will-change-transform">
        {[0, 1, 2].map((copy) => (
          <div key={copy} ref={copy === 0 ? group : undefined} aria-hidden={copy > 0} className="flex shrink-0" style={{ gap }}>
            {children}
          </div>
        ))}
      </motion.div>
    </div>
  );
}

const reviews = [
  { name: "Imani Brooks", initials: "IB", hue: 262, text: "Set up in one coffee." },
  { name: "Viktor Sund", initials: "VS", hue: 20, text: "Our churn dropped in a month." },
  { name: "Carmen Ibáñez", initials: "CI", hue: 160, text: "Finally, exports that just work." },
  { name: "Tariq Aziz", initials: "TA", hue: 310, text: "The API docs read like a novel." },
  { name: "Yuki Morita", initials: "YM", hue: 80, text: "Replaced four tabs on day one." },
  { name: "Nell Fairbanks", initials: "NF", hue: 205, text: "Support that actually answers." },
];

const topics = ["Invoicing", "Usage billing", "Tax rules", "Dunning", "Multi-currency", "Revenue reports", "Webhooks", "Customer portal", "Trials", "Coupons"];

function Review({ r }: { r: (typeof reviews)[number] }) {
  return (
    <figure className="flex w-64 items-center gap-3 rounded-2xl border border-border bg-card p-3.5 shadow-sm">
      <span
        className="flex size-10 shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
        style={{ background: `linear-gradient(135deg, oklch(0.76 0.14 ${r.hue}), oklch(0.52 0.2 ${r.hue + 40}))` }}
      >
        {r.initials}
      </span>
      <div className="min-w-0">
        <blockquote className="truncate text-sm font-medium text-card-foreground">“{r.text}”</blockquote>
        <figcaption className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
          {r.name}
          <span className="flex text-amber-500" aria-label="5 out of 5">
            {[0, 1, 2, 3, 4].map((s) => (
              <Star key={s} className="size-2.5 fill-current" />
            ))}
          </span>
        </figcaption>
      </div>
    </figure>
  );
}

export default function CarouselMarqueeInfiniteDemo() {
  return (
    <div className="flex w-full max-w-[700px] flex-col gap-4">
      <Marquee>
        {reviews.map((r) => (
          <Review key={r.name} r={r} />
        ))}
      </Marquee>
      <Marquee direction="right" speed={28}>
        {[...reviews].reverse().map((r) => (
          <Review key={r.name} r={r} />
        ))}
      </Marquee>
      <Marquee speed={48} gap={10} className="mt-2">
        {topics.map((t, i) => (
          <span key={t} className={cn("rounded-full px-4 py-2 text-sm whitespace-nowrap", i % 3 === 0 ? "bg-primary text-primary-foreground" : "border border-border bg-card text-muted-foreground")}>
            {t}
          </span>
        ))}
      </Marquee>
    </div>
  );
}
```

## AI prompt

```text
Add the "Carousel Marquee Infinite" component from PixelPapa UI to this project.

What it is: An endless row that glides in either direction and eases to a stop under the pointer instead of freezing, then eases back up to speed.
Category: Carousels. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/carousel-marquee-infinite.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/carousel-marquee-infinite.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/carousel-marquee-infinite" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More carousels

- [Carousel Coverflow 3D](https://pixelpapa.com/components/carousels/carousel-coverflow-3d.md) (Pro): Covers fan out in perspective around a centred item; swipe, click a side cover or use the arrow keys and they swing through on springs.
- [Carousel Progress Autoplay](https://pixelpapa.com/components/carousels/carousel-progress-autoplay.md) (Pro): Story-style slides with segmented progress bars that fill in real time; tap either side to move, press and hold to pause.
- [Carousel Snap Drag](https://pixelpapa.com/components/carousels/carousel-snap-drag.md) (Pro): A draggable track that carries your throw's momentum to the nearest slide, with arrows, stretching dots and arrow-key control.
- [Carousel Stacked Fade](https://pixelpapa.com/components/carousels/carousel-stacked-fade.md): Slides sit in a receding stack; the front one drops away and fades while the next rises forward, on a timer that pauses on hover.
- [Carousel Thumbnail Gallery](https://pixelpapa.com/components/carousels/carousel-thumbnail-gallery.md) (Pro): A main stage with a thumbnail strip; slides push in from the direction you travel and a ring glides between thumbnails.
- [Carousel Vertical Ticker](https://pixelpapa.com/components/carousels/carousel-vertical-ticker.md): A live feed where each new row springs in at the top, the rest step down a slot and the oldest fades off the bottom, pausing on hover.
