effects#scroll#infinite
Infinite Marquee
Smooth infinite horizontal scroll of text or content that pauses on hover
Implementation Guide
# Infinite Marquee Effect
> Smooth infinite horizontal scroll of text, logos, or any content. Pauses on hover, supports variable speed and direction. Great for testimonials, partner logos, feature lists.
## Quick Start
```tsx
import InfiniteMarquee from '@/components/effects/infinite-marquee';
export default function Partners() {
return (
<div className="relative h-32">
<InfiniteMarquee items={['PARTNER 1', 'PARTNER 2', 'PARTNER 3']} />
</div>
);
}
```
## Props
| Prop | Type | Default | Description |
| ------------ | ----------------- | -------------- | ---------------------------------------------- |
| items | string[] | (sample words) | Array of text items to display |
| speed | number | 20 | Animation duration in seconds (lower = faster) |
| direction | 'left' \| 'right' | 'left' | Scroll direction |
| pauseOnHover | boolean | true | Whether to pause animation on hover |
| gap | number | 48 | Gap between items in pixels |
## Full Implementation
```tsx
'use client';
import { useEffect, useRef, useState } from 'react';
interface InfiniteMarqueeProps {
items?: string[];
speed?: number;
direction?: 'left' | 'right';
pauseOnHover?: boolean;
gap?: number;
}
const DEFAULT_ITEMS = [
'DESIGN',
'SYSTEMS',
'BRANDING',
'TYPOGRAPHY',
'COLOR',
'IDENTITY',
'CREATIVE',
'MODERN',
];
export default function InfiniteMarquee({
items = DEFAULT_ITEMS,
speed = 20,
direction = 'left',
pauseOnHover = true,
gap = 48,
}: InfiniteMarqueeProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setPrefersReducedMotion(event.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
const shouldPause = (pauseOnHover && isHovered) || prefersReducedMotion;
const animationDirection = direction === 'left' ? 'normal' : 'reverse';
const adjustedSpeed = prefersReducedMotion ? speed * 5 : speed;
const renderItems = () => (
<div className="flex shrink-0 items-center" style={{ gap: `${gap}px` }} aria-hidden="true">
{items.map((item, index) => (
<span
key={index}
className="font-display text-4xl font-bold tracking-wider whitespace-nowrap text-white/90 uppercase md:text-6xl lg:text-7xl"
style={{
textShadow: '0 0 40px rgba(255, 255, 255, 0.1)',
}}
>
{item}
</span>
))}
</div>
);
return (
<div
ref={containerRef}
className="relative flex h-full w-full items-center overflow-hidden"
style={{ background: 'linear-gradient(135deg, #0a0a0a 0%, #111118 50%, #0a0a0a 100%)' }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Gradient fade on edges */}
<div className="pointer-events-none absolute top-0 left-0 z-10 h-full w-24 bg-gradient-to-r from-[#0a0a0a] to-transparent" />
<div className="pointer-events-none absolute top-0 right-0 z-10 h-full w-24 bg-gradient-to-l from-[#0a0a0a] to-transparent" />
{/* Scrolling container */}
<div
className="animate-marquee-scroll flex"
style={{
gap: `${gap}px`,
animationDuration: `${adjustedSpeed}s`,
animationPlayState: shouldPause ? 'paused' : 'running',
animationDirection,
}}
>
{/* Render items twice for seamless loop */}
{renderItems()}
{renderItems()}
</div>
<style jsx>{`
@keyframes marquee-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-marquee-scroll {
animation: marquee-scroll linear infinite;
}
`}</style>
</div>
);
}
```
## Customization
### Partner Logos as Text
Display partner or client names in a scrolling banner:
```tsx
<InfiniteMarquee
items={['ACME CORP', 'GLOBEX', 'INITECH', 'UMBRELLA', 'STARK INDUSTRIES']}
speed={30}
gap={80}
/>
```
### Fast Feature List
Quick-scrolling feature highlights:
```tsx
<InfiniteMarquee
items={['FAST', 'SECURE', 'RELIABLE', 'SCALABLE', 'MODERN']}
speed={10}
direction="right"
/>
```
### Testimonial Snippets
Slower scroll for readable testimonials:
```tsx
<InfiniteMarquee
items={[
'"Best tool ever" - User A',
'"Changed my workflow" - User B',
'"Highly recommended" - User C',
]}
speed={40}
pauseOnHover={true}
/>
```
### Bidirectional Double Marquee
Stack two marquees going opposite directions:
```tsx
<div className="flex flex-col">
<div className="h-24">
<InfiniteMarquee items={['ROW 1 ITEM A', 'ROW 1 ITEM B']} direction="left" />
</div>
<div className="h-24">
<InfiniteMarquee items={['ROW 2 ITEM A', 'ROW 2 ITEM B']} direction="right" />
</div>
</div>
```
## Performance Considerations
- **CSS-only animation**: Uses pure CSS transforms for GPU-accelerated performance
- **No JavaScript animation loop**: Unlike canvas-based effects, this uses CSS `animation` which is highly optimized
- **Minimal DOM**: Only duplicates content once for seamless looping
- **`will-change` optimization**: The browser automatically optimizes animated transforms
- **Avoid excessive items**: While performant, very long item lists (50+) may impact memory
## Accessibility
- **Respects `prefers-reduced-motion`**: Animation slows dramatically (5x slower) when user prefers reduced motion
- **`aria-hidden="true"`**: Duplicated content is hidden from screen readers to prevent double-reading
- **Pause on hover**: Allows users to stop motion to read content
- **No autoplay audio**: This is a visual-only effect
- **Ensure content is available elsewhere**: Since marquee content scrolls continuously, ensure important information is also presented in a static format
## Browser Support
- All modern browsers (Chrome, Firefox, Safari, Edge)
- CSS `animation` property support required
- CSS `transform: translateX()` support required
- `prefers-reduced-motion` media query support (graceful degradation in older browsers)
- Works on mobile devices with touch (hover pause activates on touch)