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