Skip to content

Marquee Ticker

An infinite scrolling strip of words or logos that pauses when you hover it.

Freeeditorialminimal
Category
Components
Added
2026-08-21
Deps
none
Updated
2026-09-01
Preview
Design•Ship•Iterate•Repeat•Vibe Code•Design•Ship•Iterate•Repeat•Vibe Code•

Props

24
Pause on hover
No runtime dependencies
"use client";

const ITEMS = ["Design", "Ship", "Iterate", "Repeat", "Vibe Code"];

export function MarqueeTicker({
  items = ITEMS.join(", "),
  speed = 24,
  direction = "left",
  pauseOnHover = true,
}: {
  /** Comma-separated. */
  items?: string;
  speed?: number;
  direction?: "left" | "right";
  pauseOnHover?: boolean;
}) {
  const words = items
    .split(",")
    .map((w) => w.trim())
    .filter(Boolean);
  const track = [...words, ...words];

  return (
    <div className="group relative w-full overflow-hidden py-3">
      <div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-10 bg-gradient-to-r from-white to-transparent" />
      <div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-10 bg-gradient-to-l from-white to-transparent" />
      <div
        className={
          "flex w-max gap-8 " +
          (pauseOnHover ? "group-hover:[animation-play-state:paused]" : "")
        }
        style={{
          animation: `marquee ${speed}s linear infinite`,
          animationDirection: direction === "right" ? "reverse" : "normal",
        }}
      >
        {track.map((item, i) => (
          <span
            key={`${item}-${i}`}
            className="flex items-center gap-8 text-2xl font-bold tracking-tight whitespace-nowrap"
          >
            {item}
            <span className="text-red-500">&bull;</span>
          </span>
        ))}
      </div>
      <style jsx>{`
        @keyframes marquee {
          from { transform: translateX(0); }
          to { transform: translateX(-50%); }
        }
      `}</style>
    </div>
  );
}
Notes

About this component

An infinite ticker needs a loop where the seam never shows, and this fakes it with one trick: the item array is duplicated exactly once, and the doubled track animates translateX from 0 to -50%, landing precisely where the second copy begins so it can snap back to 0 with no visible jump — as long as the two halves stay pixel-identical in width. That's the fragile part: async-loaded content with no fixed dimensions between the two copies would make the seam visible as a stutter. It's pure CSS, no requestAnimationFrame or interval driving it, so it costs nothing per frame regardless of what else is re-rendering on the page; reach for a JS-driven approach only if the scroll speed needs to react to something dynamic, like page scroll position, which a keyframe can't. Pause-on-hover is one Tailwind arbitrary-value class, no event handlers. The direction prop doesn't define a second, mirrored keyframe for "right" — it just replays the same keyframe with animationDirection: reverse, a subtler and cheaper way to get the same result.

Curator’s note

Pure CSS animation, not JS-driven — stays smooth at 60fps even with a dozen items and zero re-renders.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Pro

Oversized, near-invisible ticker rows scrolling behind the fold as ambient texture.

tickermarqueecss
Featured
New

A marquee that accelerates with scroll speed and flips direction when you scroll back.

marqueescrollvelocity
NewPro

A sticky section nav whose items fill like progress bars as you read — and still reaches the short last section that fixed-line navs never activate.

navigationscrollsticky