# Carousel Vertical Ticker — React Carousel Component

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

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

## Install

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

The file is written to `components/pixelpapa/carousel-vertical-ticker.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 { Pause, Play } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useState, type FocusEvent, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type VerticalTickerProps<T> = {
  /** Needs at least `visible + 2` items to loop cleanly. */
  items: T[];
  /** `slot` is the row's current position, 0 being the newest. */
  renderItem: (item: T, slot: number) => ReactNode;
  visible?: number;
  /** Milliseconds between steps. */
  interval?: number;
  itemHeight?: number;
  gap?: number;
  className?: string;
};

export function VerticalTicker<T>({ items, renderItem, visible = 4, interval = 2200, itemHeight = 62, gap = 8, className }: VerticalTickerProps<T>) {
  const n = items.length;
  const [head, setHead] = useState(0);
  const [hover, setHover] = useState(false);
  const [stopped, setStopped] = useState(false);
  const reduce = useReducedMotion();
  const step = itemHeight + gap;
  const running = !hover && !stopped && !reduce;

  useEffect(() => {
    if (!running) return;
    const t = setInterval(() => setHead((h) => (h + 1) % n), interval);
    return () => clearInterval(t);
  }, [running, interval, n]);

  function onBlur(e: FocusEvent<HTMLDivElement>) {
    if (!e.currentTarget.contains(e.relatedTarget)) setHover(false);
  }

  return (
    <div className={cn("relative", className)} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} onFocus={() => setHover(true)} onBlur={onBlur}>
      <ul
        aria-live="off"
        className="relative overflow-hidden [mask-image:linear-gradient(black_78%,transparent)]"
        style={{ height: visible * step - gap + itemHeight * 0.45 }}
      >
        {items.map((item, i) => {
          const slot = (i + head) % n;
          // The last slot is the wings: parked above, ready to drop in as the newest row.
          const waiting = slot === n - 1;
          const shown = slot < visible + 1;
          return (
            <motion.li
              key={i}
              aria-hidden={!shown}
              className="absolute inset-x-0 top-0"
              style={{ height: itemHeight }}
              initial={false}
              animate={{
                y: waiting ? -step : Math.min(slot, visible + 1) * step,
                opacity: waiting || !shown ? 0 : 1,
                scale: waiting ? 0.94 : 1,
              }}
              transition={shown || waiting ? { type: "spring", stiffness: 300, damping: 30 } : { duration: 0 }}
            >
              {renderItem(item, slot)}
            </motion.li>
          );
        })}
      </ul>
      <button
        type="button"
        aria-label={stopped ? "Resume feed" : "Pause feed"}
        aria-pressed={stopped}
        onClick={() => setStopped((s) => !s)}
        className="absolute right-1 -bottom-1 flex size-8 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-sm outline-none transition hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
      >
        {stopped ? <Play className="size-3.5 fill-current" /> : <Pause className="size-3.5 fill-current" />}
      </button>
    </div>
  );
}

const sales = [
  { who: "Mara Kowalski", initials: "MK", hue: 262, what: "upgraded to Studio", amount: 29, city: "Gdańsk" },
  { who: "Deshawn Ellis", initials: "DE", hue: 30, what: "bought 5 extra seats", amount: 45, city: "Atlanta" },
  { who: "Aiko Nakamura", initials: "AN", hue: 330, what: "started annual Solo", amount: 115, city: "Osaka" },
  { who: "Leandro Pires", initials: "LP", hue: 150, what: "renewed Studio", amount: 290, city: "Porto" },
  { who: "Freya Lindholm", initials: "FL", hue: 200, what: "added the audit log", amount: 19, city: "Malmö" },
  { who: "Omar Haddad", initials: "OH", hue: 80, what: "upgraded to Scale", amount: 99, city: "Amman" },
  { who: "Chloe Bernard", initials: "CB", hue: 295, what: "bought a template pack", amount: 12, city: "Lyon" },
];

const ages = ["just now", "12s ago", "41s ago", "1m ago", "2m ago", "3m ago"];

export default function CarouselVerticalTickerDemo() {
  return (
    <div className="w-full max-w-md rounded-[28px] border border-border bg-card p-5 shadow-xl shadow-primary/5">
      <div className="mb-4 flex items-center justify-between">
        <h3 className="flex items-center gap-2.5 text-sm font-medium text-card-foreground">
          <span className="relative flex size-2.5">
            <span className="absolute inset-0 animate-ping rounded-full bg-emerald-500/70 motion-reduce:animate-none" />
            <span className="relative size-2.5 rounded-full bg-emerald-500" />
          </span>
          Live sales
        </h3>
        <p className="text-xs text-muted-foreground">Today · $8,412</p>
      </div>
      <VerticalTicker
        items={sales}
        visible={4}
        renderItem={(s, slot) => (
          <div className={cn("flex h-full items-center gap-3 rounded-2xl border px-3.5 transition-colors duration-500", slot === 0 ? "border-primary/30 bg-primary/[0.06]" : "border-transparent bg-muted/60")}>
            <span
              className="flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white"
              style={{ background: `linear-gradient(135deg, oklch(0.76 0.14 ${s.hue}), oklch(0.52 0.2 ${s.hue + 40}))` }}
            >
              {s.initials}
            </span>
            <div className="min-w-0 flex-1">
              <p className="truncate text-sm text-card-foreground">
                <span className="font-medium">{s.who}</span> {s.what}
              </p>
              <p className="text-xs text-muted-foreground">
                {s.city} · {ages[Math.min(slot, ages.length - 1)]}
              </p>
            </div>
            <span className="text-sm font-medium text-emerald-600 tabular-nums dark:text-emerald-400">+${s.amount}</span>
          </div>
        )}
      />
    </div>
  );
}
```

## AI prompt

```text
Add the "Carousel Vertical Ticker" component from PixelPapa UI to this project.

What it is: 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.
Category: Carousels. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/carousel-vertical-ticker.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/carousel-vertical-ticker.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/carousel-vertical-ticker" 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 Marquee Infinite](https://pixelpapa.com/components/carousels/carousel-marquee-infinite.md): 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.
- [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.
