# List Rows That Turn Into Marquee Bands On Hover

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/list-rows-that-turn-into-marquee-bands-on-hover).

- Page, with a live preview: https://shadcn.exchange/r/weinc/list-rows-that-turn-into-marquee-bands-on-hover
- Screenshot: https://shadcn.exchange/shot/weinc/list-rows-that-turn-into-marquee-bands-on-hover/light.png
- Registry item: https://we.inc/r/list-rows-that-turn-into-marquee-bands-on-hover.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/list-rows-that-turn-into-marquee-bands-on-hover.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/list-rows-that-turn-into-marquee-bands-on-hover.tsx`

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

// Exhibitions at an invented regional gallery. Each row is a link; its band repeats the title.
const shows = [
  { title: "Salt and Silver", detail: "Early coastal photographs", dates: "Until 9 Nov" },
  { title: "The Weaving Room", detail: "Looms, samples and pattern books", dates: "14 Nov to 2 Feb" },
  { title: "Field Notes", detail: "Drawings from three botanists", dates: "From 6 Dec" },
  { title: "Glass Harbour", detail: "Blown and cast glass", dates: "Spring" },
];
const SPEED = 9; // seconds per loop inside each band
const IDLE_STEP = 2200; // ms each row stays lit in the idle demo

export default function ListRowsThatTurnIntoMarqueeBandsOnHover() {
  const root = useRef<HTMLElement>(null);
  const [demo, setDemo] = useState(0);
  const [live, setLive] = useState(false);

  useEffect(() => {
    const el = root.current;
    if (!el || live) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setDemo(-1); return; }
    // idle demo: light each row in turn until a real pointer or focus arrives; only while visible
    let timer = 0;
    const run = () => { timer = window.setInterval(() => setDemo((d) => (d + 1) % shows.length), IDLE_STEP); };
    const io = new IntersectionObserver(([e]) => { clearInterval(timer); if (e.isIntersecting) run(); });
    io.observe(el);
    const stop = () => setLive(true);
    el.addEventListener("pointerenter", stop);
    el.addEventListener("focusin", stop);
    return () => { clearInterval(timer); io.disconnect(); el.removeEventListener("pointerenter", stop); el.removeEventListener("focusin", stop); };
  }, [live]);

  return (
    <section ref={root} className="w3row bg-[#f7f6f2] px-5 py-16 text-[#1e2a36] sm:py-20" style={{ ["--w3row-speed" as string]: `${SPEED}s` }}>
      <style>{`
        .w3row-link { position: relative; display: grid; overflow: hidden; }
        .w3row-band { position: absolute; inset: 0; display: flex; align-items: center; overflow: hidden; background: #2d4a6b; color: #f7f6f2; transform: translateY(101%); transition: transform .5s cubic-bezier(.2,.8,.2,1); }
        .w3row-track { display: flex; width: max-content; animation: w3row-run var(--w3row-speed) linear infinite; animation-play-state: paused; }
        .w3row-group { display: flex; align-items: center; gap: 32px; padding-right: 32px; white-space: nowrap; font-size: clamp(26px, 4vw, 40px); font-weight: 600; letter-spacing: -0.02em; }
        .w3row-group i { font-style: normal; font-weight: 400; font-size: 0.5em; letter-spacing: .1em; text-transform: uppercase; opacity: .8; }
        .w3row-link:focus-visible { outline: 3px solid #c2703d; outline-offset: 2px; }
        .w3row-on .w3row-band, .w3row-link:focus-visible .w3row-band { transform: none; }
        .w3row-on .w3row-track, .w3row-link:focus-visible .w3row-track { animation-play-state: running; }
        @media (hover: hover) { .w3row-link:hover .w3row-band { transform: none; } .w3row-link:hover .w3row-track { animation-play-state: running; } }
        @keyframes w3row-run { to { transform: translateX(-50%); } }
        @media (prefers-reduced-motion: reduce) {
          .w3row-band { transition: none; }
          .w3row-track { animation: none; padding-left: 20px; }
        }
      `}</style>
      <div className="mx-auto max-w-5xl">
        <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">On at the Quayside Gallery</h2>
          <p className="text-sm text-[#4b5a69]">Open Tuesday to Sunday, entry free</p>
        </div>
        <ul className="mt-10 border-t border-[#cfd3d6]">
          {shows.map((s, i) => (
            <li key={s.title} className="border-b border-[#cfd3d6]">
              <a href="#" className={`w3row-link ${!live && demo === i ? "w3row-on" : ""}`} onMouseEnter={() => setLive(true)}>
                <span className="grid gap-1 px-1 py-6 sm:grid-cols-[1.3fr_1fr_auto] sm:items-baseline sm:gap-6">
                  <span className="text-2xl font-semibold tracking-tight sm:text-3xl">{s.title}</span>
                  <span className="text-[15px] text-[#4b5a69]">{s.detail}</span>
                  <span className="font-mono text-sm text-[#4b5a69]">{s.dates}</span>
                </span>
                <span className="w3row-band" aria-hidden="true">
                  <span className="w3row-track">
                    {[0, 1].map((k) => (
                      <span key={k} className="w3row-group">
                        {[0, 1, 2].map((n) => (
                          <span key={n} className="flex items-center gap-8">{s.title}<i>{s.dates}</i></span>
                        ))}
                      </span>
                    ))}
                  </span>
                </span>
              </a>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}
```
