Timace

Marquee

NorthwindHalcyonParallelLumenFieldnoteQuartz
components/ui/marquee.tsx
import type { ReactNode } from "react";

export interface MarqueeProps {
  children: ReactNode;
  /** Runs right to left by default. This turns it round. */
  reverse?: boolean;
  className?: string;
}

/**
 * A row that never ends: what is inside it slides past and comes round again.
 * The row is drawn twice so the second copy fills in as the first leaves, and
 * the edges fade so nothing is cut off mid-word.
 */
export function Marquee({ children, reverse, className }: MarqueeProps) {
  return (
    <div
      data-slot="marquee"
      className={[
        "group flex w-full overflow-hidden font-(family-name:--tui-font) text-(--tui-marquee-fg) [mask-image:linear-gradient(to_right,transparent,black_var(--tui-marquee-fade),black_calc(100%_-_var(--tui-marquee-fade)),transparent)]",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
    >
      {[0, 1].map((copy) => (
        <div
          key={copy}
          aria-hidden={copy === 1}
          className={`flex shrink-0 items-center gap-(--tui-marquee-gap) pr-(--tui-marquee-gap) [animation:tui-marquee_var(--tui-marquee-duration)_linear_infinite] group-hover:[animation-play-state:var(--tui-marquee-hover)] motion-reduce:[animation:none] ${reverse ? "[animation-direction:reverse]" : ""}`}
        >
          {children}
        </div>
      ))}
    </div>
  );
}
styles/timace-ui.css
:root {
  color-scheme: light;
  --tui-bg: #ffffff;
  --tui-fg: #0a0a0a;
  --tui-muted: #f5f5f5;
  --tui-muted-fg: #737373;
  --tui-border: #e5e5e5;
  --tui-primary: #171717;
  --tui-primary-fg: #fafafa;
  --tui-ring: #a3a3a3;
  --tui-success: #16a34a;
  --tui-warning: #d97706;
  --tui-danger: #dc2626;
  --tui-radius: 8px;
  --tui-border-width: 1px;
  --tui-space: 4px;
  --tui-shadow: none;
  --tui-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --tui-text: 14px;
  --tui-duration: 150ms;

  /* Marquee */
  --tui-marquee-duration: 24s;
  --tui-marquee-hover: paused;
  --tui-marquee-gap: calc(var(--tui-space) * 12);
  --tui-marquee-fade: 56px;
  --tui-marquee-fg: var(--tui-muted-fg);
}

.dark,
[data-theme="dark"] {
  color-scheme: dark;
  --tui-bg: #0a0a0a;
  --tui-fg: #fafafa;
  --tui-muted: #1c1c1c;
  --tui-muted-fg: #a3a3a3;
  --tui-border: #2a2a2a;
  --tui-primary: #fafafa;
  --tui-primary-fg: #171717;
  --tui-ring: #737373;
  --tui-success: #22c55e;
  --tui-warning: #f59e0b;
  --tui-danger: #ef4444;
}

@keyframes tui-marquee {
  to { transform: translateX(-100%); }
}

Needs Tailwind CSS v4. Import the stylesheet once, after Tailwind.