effects#scroll#parallax

Parallax Layers

Multi-layer parallax effect creating depth on scroll

Implementation Guide
# Parallax Layers Effect

> Multi-layer parallax effect creating depth on scroll and mouse movement. Layered elements move at different speeds to create a 3D depth illusion.

## Quick Start

```tsx
import ParallaxLayers from '@/components/effects/parallax-layers';

export default function HeroSection() {
  return (
    <div className="relative h-screen">
      <ParallaxLayers />
      <div className="relative z-10 flex h-full items-center justify-center">
        <h1 className="text-5xl font-bold text-white">Parallax Effect</h1>
      </div>
    </div>
  );
}
```

## Props

| Prop      | Type                       | Default          | Description                     |
| --------- | -------------------------- | ---------------- | ------------------------------- |
| layers    | Layer[]                    | [default layers] | Array of layer configurations   |
| baseSpeed | number                     | 1                | Multiplier for all layer speeds |
| direction | 'vertical' \| 'horizontal' | 'vertical'       | Direction of parallax movement  |

### Layer Configuration

```tsx
interface Layer {
  speed: number; // Movement speed multiplier (0.1 = slow, 1.0 = fast)
  content: React.ReactNode; // Layer content (JSX)
  className?: string; // Additional CSS classes
}
```

## Full Implementation

```tsx
'use client';

import { useEffect, useRef, useState } from 'react';

interface Layer {
  speed: number;
  content: React.ReactNode;
  className?: string;
}

interface ParallaxLayersProps {
  layers?: Layer[];
  baseSpeed?: number;
  direction?: 'vertical' | 'horizontal';
}

const DEFAULT_LAYERS: Layer[] = [
  {
    speed: 0.1,
    content: (
      <div className="absolute inset-0 opacity-20">
        {Array.from({ length: 20 }).map((_, i) => (
          <div
            key={i}
            className="absolute h-2 w-2 rounded-full bg-white"
            style={{
              left: `${Math.random() * 100}%`,
              top: `${Math.random() * 100}%`,
            }}
          />
        ))}
      </div>
    ),
  },
  {
    speed: 0.3,
    content: (
      <div className="absolute inset-0 opacity-30">
        {Array.from({ length: 15 }).map((_, i) => (
          <div
            key={i}
            className="absolute h-4 w-4 rounded-full bg-blue-500"
            style={{
              left: `${Math.random() * 100}%`,
              top: `${Math.random() * 100}%`,
            }}
          />
        ))}
      </div>
    ),
  },
  {
    speed: 0.5,
    content: (
      <div className="absolute inset-0 opacity-40">
        {Array.from({ length: 10 }).map((_, i) => (
          <div
            key={i}
            className="absolute h-8 w-8 rounded-full bg-purple-500"
            style={{
              left: `${Math.random() * 100}%`,
              top: `${Math.random() * 100}%`,
            }}
          />
        ))}
      </div>
    ),
  },
  {
    speed: 0.7,
    content: (
      <div className="absolute inset-0 opacity-50">
        {Array.from({ length: 5 }).map((_, i) => (
          <div
            key={i}
            className="absolute h-16 w-16 rounded-full bg-red-500"
            style={{
              left: `${Math.random() * 100}%`,
              top: `${Math.random() * 100}%`,
            }}
          />
        ))}
      </div>
    ),
  },
];

export default function ParallaxLayers({
  layers = DEFAULT_LAYERS,
  baseSpeed = 1,
  direction = 'vertical',
}: ParallaxLayersProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [scrollProgress, setScrollProgress] = useState(0);

  useEffect(() => {
    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (prefersReducedMotion) return;

    const container = containerRef.current;
    if (!container) return;

    const handleScroll = () => {
      const rect = container.getBoundingClientRect();
      const windowHeight = window.innerHeight;

      // Calculate scroll progress through viewport
      const scrolled = windowHeight - rect.top;
      const totalScrollable = windowHeight + rect.height;
      const progress = Math.max(0, Math.min(1, scrolled / totalScrollable));

      setScrollProgress(progress);
    };

    // Also respond to mouse movement within container
    const handleMouseMove = (event: MouseEvent) => {
      const rect = container.getBoundingClientRect();
      const x = (event.clientX - rect.left) / rect.width;
      const y = (event.clientY - rect.top) / rect.height;

      setScrollProgress(direction === 'vertical' ? y : x);
    };

    window.addEventListener('scroll', handleScroll, { passive: true });
    container.addEventListener('mousemove', handleMouseMove);
    handleScroll();

    return () => {
      window.removeEventListener('scroll', handleScroll);
      container.removeEventListener('mousemove', handleMouseMove);
    };
  }, [direction]);

  const getTransform = (speed: number) => {
    const offset = (scrollProgress - 0.5) * 100 * speed * baseSpeed;

    if (direction === 'vertical') {
      return `translateY(${offset}px)`;
    }
    return `translateX(${offset}px)`;
  };

  return (
    <div
      ref={containerRef}
      className="relative h-full w-full overflow-hidden"
      style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 50%, #0a0a1a 100%)' }}
    >
      {layers.map((layer, index) => (
        <div
          key={index}
          className={`absolute inset-0 transition-transform duration-100 ease-out ${layer.className || ''}`}
          style={{
            transform: getTransform(layer.speed),
            zIndex: index,
          }}
        >
          {layer.content}
        </div>
      ))}
    </div>
  );
}
```

## Customization

### Custom Layers

Create your own layer content:

```tsx
const customLayers = [
  {
    speed: 0.2,
    content: (
      <div className="absolute inset-0">
        <img src="/bg-mountains.png" className="h-full w-full object-cover opacity-50" />
      </div>
    ),
  },
  {
    speed: 0.5,
    content: (
      <div className="absolute inset-0">
        <img src="/bg-trees.png" className="h-full w-full object-cover opacity-70" />
      </div>
    ),
  },
  {
    speed: 0.8,
    content: (
      <div className="absolute inset-0">
        <img src="/bg-foreground.png" className="h-full w-full object-cover" />
      </div>
    ),
  },
];

<ParallaxLayers layers={customLayers} />;
```

### Geometric Shapes

```tsx
const geometricLayers = [
  {
    speed: 0.1,
    content: (
      <div className="absolute inset-0">
        <div className="absolute top-1/4 left-1/4 h-32 w-32 rotate-45 border border-white/20" />
        <div className="absolute right-1/3 bottom-1/3 h-24 w-24 rounded-full border border-white/20" />
      </div>
    ),
  },
  {
    speed: 0.4,
    content: (
      <div className="absolute inset-0">
        <div className="absolute top-1/2 left-1/2 h-48 w-48 -translate-x-1/2 -translate-y-1/2 border-2 border-blue-500/30" />
      </div>
    ),
  },
];
```

### Horizontal Parallax

For horizontal scrolling sections:

```tsx
<ParallaxLayers direction="horizontal" />
```

### Subtle Effect

For a more subtle background effect:

```tsx
<ParallaxLayers baseSpeed={0.3} />
```

## Performance Considerations

- Use `transform` instead of `top/left` for GPU acceleration
- Limit number of layers (4-6 max recommended)
- Use `passive: true` on scroll listeners
- Add `will-change: transform` on layer elements for smoother animation
- Consider using `requestAnimationFrame` for very smooth animations

## Accessibility

- Respects `prefers-reduced-motion` - parallax effect is disabled
- Ensure text content has sufficient contrast over all layers
- Don't rely on parallax motion to convey important information

## Advanced: Image-based Layers

For landscape/scene parallax:

```tsx
const sceneLayers = [
  { speed: 0.1, content: <img src="/sky.png" className="..." /> },
  { speed: 0.2, content: <img src="/mountains-far.png" className="..." /> },
  { speed: 0.4, content: <img src="/mountains-mid.png" className="..." /> },
  { speed: 0.6, content: <img src="/trees.png" className="..." /> },
  { speed: 0.8, content: <img src="/foreground.png" className="..." /> },
];
```

Tips for image layers:

- Use transparent PNGs
- Ensure images are wider/taller than container to allow movement
- Order by depth (slowest = furthest back)
- Use `object-cover` and `object-position` for responsive sizing