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)