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 liveComponent 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 JSXimport 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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| items | ReactNode[] | Required | The logos, badges or cards to scroll. |
| reverse | boolean | false | Scrolls right to left when false, left to right when true. |
| duration | number (seconds) | 28 | Time for one full loop. Larger numbers scroll slower. |
| pauseOnHover | boolean | true | Pauses the track while the pointer is over it. |
| label | string | Required | Accessible 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
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.
More UI Components & Patterns
Tweet Card
Verified social testimonial card with live engagement metrics, media embed, and responsive theme support.
Inspect Component Layouts & GridsBento Grid
Asymmetric feature showcase grid with ambient radial spotlight, badge headers, and interactive hover states.
Inspect Component Animations & FeedsAnimated List
Dynamic notification and activity feed with staggered entrance transitions, auto-flow, and pause controls.
Inspect ComponentHigh-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.