# Outlined Word Marquee Rows In Opposite Directions

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/outlined-word-marquee-rows-in-opposite-directions).

- Page, with a live preview: https://shadcn.exchange/r/weinc/outlined-word-marquee-rows-in-opposite-directions
- Screenshot: https://shadcn.exchange/shot/weinc/outlined-word-marquee-rows-in-opposite-directions/light.png
- Registry item: https://we.inc/r/outlined-word-marquee-rows-in-opposite-directions.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/outlined-word-marquee-rows-in-opposite-directions.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/outlined-word-marquee-rows-in-opposite-directions.tsx`

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

// Two rows of big words for a neighbourhood bakery. The top row runs left, the bottom row runs right.
const rowA = ["Country sourdough", "Seeded rye", "Morning buns", "Spelt loaf"];
const rowB = ["Baked before six", "Milled nearby", "Long ferment", "Wood fired"];
const SPEED = 42; // seconds per loop; the second row runs a little slower

function Row({ words, dir }: { words: string[]; dir: "l" | "r" }) {
  const group = (dup: boolean) => (
    <span className="w3wrd-group" aria-hidden={dup ? "true" : undefined}>
      {words.map((w, i) => (
        <span key={w} className="flex items-center gap-8 sm:gap-12">
          <span className={i % 2 ? "w3wrd-line" : "w3wrd-fill"}>{w}</span>
          <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" className="shrink-0 fill-[#b5562f]"><circle cx="10" cy="10" r="6" /></svg>
        </span>
      ))}
    </span>
  );
  return (
    <div className={`w3wrd-track w3wrd-${dir}`}>
      {group(false)}
      {group(true)}
    </div>
  );
}

export default function OutlinedWordMarqueeRowsInOppositeDirections() {
  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="w3wrd overflow-hidden bg-[#f3ece0] py-16 text-[#2b2118] sm:py-20"
      style={{ ["--w3wrd-speed" as string]: `${SPEED}s` }}
    >
      <style>{`
        .w3wrd-view { overflow: hidden; }
        .w3wrd-track { display: flex; width: max-content; animation: w3wrd-run var(--w3wrd-speed) linear infinite; }
        .w3wrd-r { animation-direction: reverse; animation-duration: calc(var(--w3wrd-speed) * 1.25); }
        .w3wrd-group { display: flex; align-items: center; gap: 2rem; padding-right: 2rem; white-space: nowrap; font-family: ui-serif, Georgia, serif; font-size: clamp(44px, 8vw, 104px); line-height: 1.1; letter-spacing: -0.03em; }
        @media (min-width: 640px) { .w3wrd-group { gap: 3rem; padding-right: 3rem; } }
        .w3wrd-fill { color: #2b2118; }
        .w3wrd-line { color: transparent; -webkit-text-stroke: 1.5px #2b2118; font-style: italic; }
        .w3wrd[data-paused] .w3wrd-track, .w3wrd[data-off] .w3wrd-track, .w3wrd-view:hover .w3wrd-track { animation-play-state: paused; }
        @keyframes w3wrd-run { to { transform: translateX(-50%); } }
        @media (prefers-reduced-motion: reduce) {
          .w3wrd-track { animation: none; width: auto; padding: 0 20px; }
          .w3wrd-group { flex-wrap: wrap; white-space: normal; row-gap: 0; font-size: clamp(34px, 6vw, 64px); }
          .w3wrd-group[aria-hidden] { display: none; }
          .w3wrd-toggle, .w3wrd-r { display: none; }
        }
      `}</style>
      <div className="mx-auto flex max-w-6xl flex-col gap-2 px-5 sm:flex-row sm:items-end sm:justify-between">
        <h2 className="text-sm font-semibold uppercase tracking-[0.25em] text-[#7a4a2c]">Hollis Street Bakery</h2>
        <p className="max-w-sm text-base leading-relaxed text-[#4a3b2e]">Country sourdough, seeded rye, spelt and morning buns, from a wood oven lit at four.</p>
      </div>
      <div className="w3wrd-view mt-10 space-y-2 sm:mt-14" aria-hidden="true">
        <Row words={rowA} dir="l" />
        <Row words={rowB} dir="r" />
      </div>
      <div className="mx-auto mt-10 flex max-w-6xl px-5">
        <button
          type="button"
          aria-pressed={paused}
          onClick={() => setPaused((p) => !p)}
          className="w3wrd-toggle inline-flex h-11 items-center gap-2 rounded-full bg-[#2b2118] px-5 text-sm font-medium text-[#f3ece0] hover:bg-[#3d2f23] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2b2118] focus-visible:ring-offset-2 focus-visible:ring-offset-[#f3ece0]"
        >
          {paused ? "Play the words" : "Pause the words"}
        </button>
      </div>
    </section>
  );
}
```
