Marquee - Infinite Scroll Component

An infinite scrolling marquee for text, images, logos, or any HTML. Supports left, right, up, and down directions with pause-on-hover, edge fade, and 3D layouts.

Installation

npx shadcn@latest add "https://ui-struct.vercel.app/r/marquee"

Add these animations to your tailwind.config.ts (if not already present):

// theme.extend.animation
marquee: 'marquee var(--duration) linear infinite',
'marquee-vertical': 'marquee-vertical var(--duration) linear infinite',
// theme.extend.keyframes
marquee: {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(calc(-100% - var(--gap)))' },
},
'marquee-vertical': {
from: { transform: 'translateY(0)' },
to: { transform: 'translateY(calc(-100% - var(--gap)))' },
},

Usage

import Marquee from "@/components/ui/marquee";
// or: import { Marquee } from "@/components/ui/marquee";
<Marquee pauseOnHover>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Marquee>

Preview

Two horizontal rows scrolling in opposite directions. Hover to pause.

I've never seen anything like this before. It's amazing. I love it.
Jack Morrison

Product Designer·@jack

I don't know what to say. I'm speechless. This is amazing.
Jill Harper

Frontend Engineer·@jill

I'm at a loss for words. This is amazing. I love it.
John Blake

Founder·@john

This is hands down the best UI library I've used. Incredible.
Jane Ortiz

Design Lead·@jane

Clean, fast, and beautiful. Exactly what I needed for my project.
Jenny Cole

Full-stack Dev·@jenny

I'm at a loss for words. This is amazing. I love it.
James Wu

CTO·@james

Directions

Scroll left, right, up, or down with the direction prop.

Left (default)

React
Next.js
Tailwind
Framer
TypeScript
Radix

Right

React
Next.js
Tailwind
Framer
TypeScript
Radix

Up

React
Next.js
Tailwind
Framer
TypeScript
Radix

Down

React
Next.js
Tailwind
Framer
TypeScript
Radix

Direction Options

DirectionDescription
leftHorizontal, moving left (default feel)
rightHorizontal, moving right
upVertical, moving up
downVertical, moving down

You can also use vertical and reverse instead of direction.

Vertical

Side-by-side vertical marquees — great for testimonials.

I've never seen anything like this before. It's amazing. I love it.
Jack Morrison

Product Designer·@jack

I don't know what to say. I'm speechless. This is amazing.
Jill Harper

Frontend Engineer·@jill

I'm at a loss for words. This is amazing. I love it.
John Blake

Founder·@john

This is hands down the best UI library I've used. Incredible.
Jane Ortiz

Design Lead·@jane

Clean, fast, and beautiful. Exactly what I needed for my project.
Jenny Cole

Full-stack Dev·@jenny

I'm at a loss for words. This is amazing. I love it.
James Wu

CTO·@james

3D Perspective

Stack vertical marquees with a 3D transform for a depth effect.

I've never seen anything like this before. It's amazing. I love it.
Jack Morrison

Product Designer·@jack

I don't know what to say. I'm speechless. This is amazing.
Jill Harper

Frontend Engineer·@jill

I'm at a loss for words. This is amazing. I love it.
John Blake

Founder·@john

This is hands down the best UI library I've used. Incredible.
Jane Ortiz

Design Lead·@jane

Clean, fast, and beautiful. Exactly what I needed for my project.
Jenny Cole

Full-stack Dev·@jenny

I'm at a loss for words. This is amazing. I love it.
James Wu

CTO·@james

I've never seen anything like this before. It's amazing. I love it.
Jack Morrison

Product Designer·@jack

I don't know what to say. I'm speechless. This is amazing.
Jill Harper

Frontend Engineer·@jill

I'm at a loss for words. This is amazing. I love it.
John Blake

Founder·@john

This is hands down the best UI library I've used. Incredible.
Jane Ortiz

Design Lead·@jane

Clean, fast, and beautiful. Exactly what I needed for my project.
Jenny Cole

Full-stack Dev·@jenny

I'm at a loss for words. This is amazing. I love it.
James Wu

CTO·@james

Text

Works with any HTML — logos, badges, or plain text.

React
Next.js
Vercel
Tailwind
TypeScript
Framer
Radix
shadcn/ui

Icon with Text

Skill tags and cards with brand icons — great for tech stacks and tool belts.

TypeScript
React
Next.js
Tailwind
Framer
Node.js
Vite
Vue
Python
Docker
GraphQL
PostgreSQL
Prisma
Figma
GitHub
Vercel

Props

PropTypeDefaultDescription
childrenReactNode-Items to scroll (text, images, or any HTML)
direction'left' | 'right' | 'up' | 'down'-Scroll direction (overrides vertical / reverse)
verticalbooleanfalseVertical orientation
reversebooleanfalseReverse animation direction
pauseOnHoverbooleanfalsePause while hovering
repeatnumber4Copies of children for a seamless loop
durationnumber40Animation duration in seconds
gapstring'1rem'Gap between items
fadebooleantrueSoft fade on the edges
classNamestring-Additional CSS classes

Features

  • Any content — Text, images, cards, or custom HTML
  • Four directions — Left, right, up, down
  • Pause on hover — Optional interaction-friendly pause
  • Edge fade — Masked soft edges for a polished look
  • Lightweight — Pure CSS animation, no JS loop
  • Accessible — Duplicate tracks marked aria-hidden

Full Code

'use client';
import React from 'react';
import { cn } from '@/lib/utils';
export type MarqueeDirection = 'left' | 'right' | 'up' | 'down';
export interface MarqueeProps {
children: React.ReactNode;
className?: string;
/** Scroll direction. Overrides `vertical` / `reverse` when set. */
direction?: MarqueeDirection;
/** Vertical scroll (ignored when `direction` is set) */
vertical?: boolean;
/** Reverse animation direction (ignored when `direction` is set) */
reverse?: boolean;
/** Pause animation while hovered */
pauseOnHover?: boolean;
/** How many times to repeat children for a seamless loop */
repeat?: number;
/** Animation duration in seconds */
duration?: number;
/** Gap between items (CSS length) */
gap?: string;
/** Fade edges with a mask */
fade?: boolean;
}
function resolveOrientation(
direction: MarqueeDirection | undefined,
vertical: boolean,
reverse: boolean
) {
if (direction) {
return {
isVertical: direction === 'up' || direction === 'down',
isReverse: direction === 'right' || direction === 'down',
};
}
return { isVertical: vertical, isReverse: reverse };
}
function Marquee({
children,
className,
direction,
vertical = false,
reverse = false,
pauseOnHover = true,
repeat = 4,
duration = 40,
gap = '1rem',
fade = true,
}: MarqueeProps) {
const { isVertical, isReverse } = resolveOrientation(
direction,
vertical,
reverse
);
const maskImage = fade
? isVertical
? 'linear-gradient(to bottom, transparent, black 12%, black 88%, transparent)'
: 'linear-gradient(to right, transparent, black 12%, black 88%, transparent)'
: undefined;
return (
<div
className={cn(
'group flex overflow-hidden p-2',
isVertical ? 'flex-col' : 'flex-row',
className
)}
style={
{
'--duration': `${duration}s`,
'--gap': gap,
gap: 'var(--gap)',
maskImage,
WebkitMaskImage: maskImage,
} as React.CSSProperties
}
>
{Array.from({ length: repeat }).map((_, i) => (
<div
key={i}
className={cn(
'flex shrink-0 justify-around',
isVertical
? 'animate-marquee-vertical flex-col'
: 'animate-marquee flex-row',
isReverse && '[animation-direction:reverse]',
pauseOnHover && 'group-hover:[animation-play-state:paused]'
)}
style={{ gap: 'var(--gap)' }}
aria-hidden={i > 0 || undefined}
>
{children}
</div>
))}
</div>
);
}
export { Marquee };
// @docs-preview-start
export default function MarqueeDocsPreview() {
const items = ['React', 'Next.js', 'Tailwind', 'TypeScript', 'Framer'];
return (
<div className="relative flex h-[160px] w-full items-center justify-center overflow-hidden">
<Marquee pauseOnHover duration={20}>
{items.map((item) => (
<div
key={item}
className="rounded-full border border-zinc-200 bg-zinc-50 px-4 py-1.5 text-sm font-medium text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200"
>
{item}
</div>
))}
</Marquee>
</div>
);
}
// @docs-preview-end