vnhax
Motion & DisplayMagicUI PatternWCAG 2.1 AA Compliant · Zero-Dependency

Infinite Marquee

Seamless scrolling strip for logos, tools or testimonials, built with pure CSS, pause on hover and reduced-motion support.

Interactive Live Preview

Test responsive viewport scaling & interactions live

Hover a row to pause it, or use the button. Motion stops automatically if your system asks for reduced motion.

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • Vercel
  • Supabase
  • Prisma
  • Vite
  • Claude Code
  • Codex
  • Ollama
  • llama.cpp
  • vLLM
  • LangChain
  • Hugging Face
  • MCP
Engineering Specification

Component Architecture & Rationale

The Infinite Marquee scrolls a row of items, such as partner logos, tech stack badges or short testimonials, in a smooth endless loop. It is a common "trusted by" or "built with" strip on landing pages. This version runs on a single CSS keyframe animation, so it needs no animation library and no JavaScript timer, and it stops moving for visitors who have asked their system for reduced motion.

The trick is duplication: the list is rendered twice inside one flex track, and the track is moved left by exactly half its width (plus half the gap). When the first copy has scrolled out, the second copy sits exactly where the first one started, so the loop has no visible jump. A CSS mask fades both edges. The duplicate copy is marked aria-hidden so screen readers only hear each item once, and animation-play-state pauses the track on hover.

Key Technical Features

  • ✓Pure CSS animation: no JavaScript timers, no animation library
  • ✓Seamless loop by rendering the list twice and translating by half the track
  • ✓Pause on hover, plus an optional pause button for users who need it
  • ✓Respects prefers-reduced-motion by stopping the animation and wrapping items
  • ✓Reverse direction and speed controlled with one prop each

Complete Production Source Code

Standalone, dependency-free implementation. Drop directly into your project.

1. React 19 / TypeScript Component (InfiniteMarquee.tsx)

TypeScript + Semantic JSX
import React from 'react';
import './marquee.css';

type MarqueeProps = {
  items: React.ReactNode[];
  label: string;
  reverse?: boolean;
  duration?: number;
  pauseOnHover?: boolean;
};

export function Marquee({ items, label, reverse = false, duration = 28, pauseOnHover = true }: MarqueeProps) {
  const classes = ['marquee'];
  if (reverse) classes.push('marquee--reverse');
  if (pauseOnHover) classes.push('marquee--hover-pause');

  return (
    <div className={classes.join(' ')} role="region" aria-label={label}>
      <ul className="marquee__track" style={{ animationDuration: duration + 's' }}>
        {items.concat(items).map((item, i) => (
          <li key={i} className="marquee__item" aria-hidden={i >= items.length}>
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}

2. Vanilla CSS Stylesheet (Zero-Dependency)

Native CSS Tokens & Transitions
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(calc(-50% - 6px)); } /* half the track + half the gap */
}
.marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.marquee__track {
  display: flex;
  gap: 12px;
  width: max-content;
  margin: 0;
  padding: 0;
  list-style: none;
  animation: marquee-scroll 28s linear infinite;
}
.marquee--reverse .marquee__track { animation-direction: reverse; }
.marquee--hover-pause:hover .marquee__track { animation-play-state: paused; }
.marquee__item {
  flex-shrink: 0;
  padding: 10px 18px;
  border: 1px solid #e2e8f0;
  border-radius: 999px;
  background: #ffffff;
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; flex-wrap: wrap; width: auto; }
}

3. Tailwind CSS Classes Variant

Tailwind v3 & v4 Utility Equivalents
<!-- Tailwind: add the keyframes to tailwind.config (theme.extend.keyframes / animation) -->
<!-- keyframes: marquee { from { transform: translateX(0) } to { transform: translateX(calc(-50% - 0.375rem)) } } -->
<div class="group overflow-hidden [mask-image:linear-gradient(90deg,transparent,#000_10%,#000_90%,transparent)]" role="region" aria-label="Tools we write about">
  <ul class="flex w-max gap-3 animate-[marquee_28s_linear_infinite] group-hover:[animation-play-state:paused] motion-reduce:animate-none">
    <li class="shrink-0 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-semibold">Next.js</li>
    <li class="shrink-0 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-semibold">React</li>
    <!-- ...repeat the same items a second time, with aria-hidden="true" -->
  </ul>
</div>

Installation & Integration Guide

Steps to add this component to Next.js (App Router), Vite, or Astro projects.

CLI Installation CommandShadcn / MagicUI Compatible
npx shadcn@latest add "https://magicui.design/r/marquee"

Step 1: Copy the component

# No dependencies. Copy Marquee.tsx and marquee.css into your project,
# or use the MagicUI registry command above if you already use shadcn/ui.

Step 2: Use it on a page

Keep items short. A marquee is for recognition (logos, names), not for text people need to read carefully.

import { Marquee } from '@/components/Marquee';

<Marquee
  label="Tools we write about"
  items={['Next.js', 'React', 'Ollama', 'vLLM']}
  duration={30}
/>

Props & API Reference

TypeScript interfaces, default values, and runtime behavior.

PropTypeDefaultDescription
itemsReactNode[]RequiredThe logos, badges or cards to scroll.
reversebooleanfalseScrolls right to left when false, left to right when true.
durationnumber (seconds)28Time for one full loop. Larger numbers scroll slower.
pauseOnHoverbooleantruePauses the track while the pointer is over it.
labelstringRequiredAccessible name for the region, e.g. "Companies using our product".

Accessibility & Core Web Vitals Audit

♿

Designed for WCAG 2.2.2 (Pause, Stop, Hide)

ARIA Semantic Annotations

  • The strip is a labelled region (role="region" with aria-label) so screen-reader users know what it contains.
  • The duplicated copy of the items is aria-hidden, so each item is announced once.
  • Content that moves for more than 5 seconds needs a way to pause it (WCAG 2.2.2): hover pause plus an explicit pause button covers this.

Keyboard Navigation

  • The pause button is a native <button> with aria-pressed, reachable with Tab and toggled with Enter or Space.
  • Avoid putting links inside a moving marquee; moving focus targets are hard to use. Link to the full list elsewhere.
⚡

Core Web Vitals Impact

Cumulative Layout Shift
0.00 (transform-only animation does not shift layout)
Interaction to Next Paint
No JS on the main thread during scrolling

Engineering Tip: Animate only transform, never left or margin, so the browser can run it on the compositor. Give logo images fixed width and height so the track size is known before they load.

Frequently Asked Questions

How does an infinite marquee loop without a visible jump?

The items are rendered twice in one track, and the track moves left by exactly half its width (plus half the gap). When the first copy finishes, the second copy is in the same spot the first one started, so the animation restarts invisibly.

Is a scrolling marquee bad for accessibility?

It can be. WCAG 2.2.2 says moving content that lasts more than 5 seconds needs a way to pause it. Add pause on hover, an explicit pause button, and stop the animation for users with prefers-reduced-motion. Do not put essential text or links in it.

Do I need Framer Motion or GSAP for a marquee?

No. A single CSS keyframe that animates transform is enough and is cheaper to run than a JavaScript animation loop.

How do I change the speed?

Change the animation duration. With the component above, pass duration={40} for slower scrolling or duration={15} for faster.

High-Quality Interface Engineering

Every pattern in the VNHAX component directory is designed for longevity. By eliminating heavy JavaScript styling runtimes and adhering strictly to semantic HTML, your application maintains zero-shift Core Web Vitals and frictionless accessibility compliance.