# Review Card Marquee With Pause Button

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/review-card-marquee-with-pause-button).

- Page, with a live preview: https://shadcn.exchange/r/weinc/review-card-marquee-with-pause-button
- Screenshot: https://shadcn.exchange/shot/weinc/review-card-marquee-with-pause-button/light.png
- Registry item: https://we.inc/r/review-card-marquee-with-pause-button.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/review-card-marquee-with-pause-button.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/review-card-marquee-with-pause-button.tsx`

```tsx
import { useEffect, useRef, useState } from "react";

// Guest notes for an invented sea kayak outfitter. Each card is one quote.
const reviews = [
  { quote: "Our guide spotted a seal colony before any of us saw the rocks. Calm water, good jokes, hot tea at the end.", name: "Priya N.", trip: "Sunrise paddle" },
  { quote: "First time in a kayak and I never felt out of my depth. They paired me with a steady partner.", name: "Tom W.", trip: "Harbour loop" },
  { quote: "The dry bags actually kept my phone dry. Small thing, but it told me they think about details.", name: "Ines R.", trip: "Island crossing" },
  { quote: "We booked for the kids and ended up going back the next morning on our own.", name: "Dana and Lou", trip: "Family half day" },
  { quote: "They called the evening before to move us an hour earlier because of the wind. Glad they did.", name: "Marcus O.", trip: "Cliff caves" },
  { quote: "Lunch on the sand bar was the best sandwich of the whole holiday.", name: "Hana K.", trip: "Full day coast" },
];
const SPEED = 60; // seconds for one loop; slow enough to read a card in passing

function Cards({ dup }: { dup?: boolean }) {
  return (
    <ul className="w3rev-group" aria-hidden={dup ? "true" : undefined}>
      {reviews.map((r) => (
        <li key={r.name} className="w3rev-card flex w-[300px] shrink-0 flex-col justify-between rounded-2xl border border-[#e2d6c2] bg-[#fffaf1] p-6 sm:w-[340px]">
          <blockquote className="text-[15px] leading-relaxed text-[#3b3226]">
            <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" className="mb-3 fill-[#c08a4a]"><path d="M4 17h5l2-5V6H4v6h3zm10 0h5l2-5V6h-7v6h3z" /></svg>
            {r.quote}
          </blockquote>
          <p className="mt-5 flex items-center gap-3 text-sm">
            <span aria-hidden="true" className="grid h-9 w-9 place-items-center rounded-full bg-[#1f4a4f] text-xs font-semibold text-[#f6efe2]">{r.name.slice(0, 1)}</span>
            <span><span className="block font-semibold text-[#2a241c]">{r.name}</span><span className="text-[#6b5c48]">{r.trip}</span></span>
          </p>
        </li>
      ))}
    </ul>
  );
}

export default function ReviewCardMarqueeWithPauseButton() {
  const root = useRef<HTMLElement>(null);
  const [paused, setPaused] = useState(false);
  const [off, setOff] = useState(false);

  useEffect(() => {
    const el = root.current;
    if (!el) return;
    const io = new IntersectionObserver(([e]) => setOff(!e.isIntersecting));
    io.observe(el);
    return () => io.disconnect();
  }, []);

  return (
    <section
      ref={root}
      data-paused={paused ? "" : undefined}
      data-off={off ? "" : undefined}
      className="w3rev overflow-hidden bg-[#efe5d4] py-20"
      style={{ ["--w3rev-speed" as string]: `${SPEED}s` }}
    >
      <style>{`
        .w3rev-view { overflow: hidden; padding: 8px 0 16px; }
        .w3rev-track { display: flex; width: max-content; animation: w3rev-run var(--w3rev-speed) linear infinite; }
        .w3rev-group { display: flex; gap: 20px; padding: 0 20px 0 0; margin: 0; list-style: none; }
        .w3rev-card { box-shadow: 0 1px 2px rgba(60,40,10,.06), 0 12px 28px -16px rgba(60,40,10,.25); }
        .w3rev[data-paused] .w3rev-track, .w3rev[data-off] .w3rev-track, .w3rev-view:hover .w3rev-track, .w3rev-view:focus-within .w3rev-track { animation-play-state: paused; }
        @keyframes w3rev-run { to { transform: translateX(-50%); } }
        @media (prefers-reduced-motion: reduce) {
          .w3rev-view { overflow-x: auto; scroll-snap-type: x mandatory; }
          .w3rev-track { animation: none; padding: 0 20px; }
          .w3rev-card { scroll-snap-align: start; }
          .w3rev-group[aria-hidden] { display: none; }
          .w3rev-toggle { display: none; }
        }
      `}</style>
      <div className="mx-auto flex max-w-6xl flex-col gap-6 px-5 sm:flex-row sm:items-end sm:justify-between">
        <div>
          <p className="text-xs font-semibold uppercase tracking-[0.25em] text-[#8a5a24]">Tidewater Kayak Tours</p>
          <h2 className="mt-3 max-w-lg text-3xl font-semibold tracking-tight text-[#2a241c] sm:text-4xl">What paddlers wrote after a morning on the water</h2>
        </div>
        <button
          type="button"
          aria-pressed={paused}
          onClick={() => setPaused((p) => !p)}
          className="w3rev-toggle inline-flex h-11 shrink-0 items-center gap-2 self-start rounded-full border border-[#1f4a4f] px-5 text-sm font-medium text-[#1f4a4f] hover:bg-[#1f4a4f] hover:text-[#f6efe2] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1f4a4f] focus-visible:ring-offset-2 focus-visible:ring-offset-[#efe5d4] sm:self-auto"
        >
          <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" className="fill-current">
            {paused ? <path d="M4 2.5v11l9-5.5z" /> : <path d="M4 2.5h3v11H4zM9 2.5h3v11H9z" />}
          </svg>
          {paused ? "Play reviews" : "Pause reviews"}
        </button>
      </div>
      <div className="w3rev-view mt-10">
        <div className="w3rev-track">
          <Cards />
          <Cards dup />
        </div>
      </div>
    </section>
  );
}
```
