Parallax Layers
Multi-depth parallax scroll effect — foreground, midground, and background layers move at different speeds for a 2.5D feel
parallaxscrolllayersdepthframer-motion
Install dependencies
$npm install framer-motionPreview
Source Code
"use client";
import {
createContext,
useContext,
useRef,
type ReactNode,
type RefObject,
} from "react";
import {
motion,
useScroll,
useTransform,
type MotionValue,
} from "framer-motion";
interface ParallaxContextValue {
scrollYProgress: MotionValue<number>;
}
const ParallaxContext = createContext<ParallaxContextValue | null>(null);
interface ParallaxLayersProps {
height?: string;
className?: string;
/** Optional scrollable ancestor to track scroll progress against. Defaults to window. */
container?: RefObject<HTMLElement | null>;
children: ReactNode;
}
export function ParallaxLayers({
height = "100vh",
className = "",
container,
children,
}: ParallaxLayersProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
container,
offset: ["start end", "end start"],
});
return (
<ParallaxContext.Provider value={{ scrollYProgress }}>
<div
ref={ref}
style={{ height }}
className={`relative overflow-hidden ${className}`}
>
{children}
</div>
</ParallaxContext.Provider>
);
}
interface ParallaxLayerProps {
speed?: number;
className?: string;
children: ReactNode;
}
export function ParallaxLayer({
speed = 0.5,
className = "",
children,
}: ParallaxLayerProps) {
const ctx = useContext(ParallaxContext);
if (!ctx) {
throw new Error("ParallaxLayer must be used inside <ParallaxLayers>");
}
const y = useTransform(ctx.scrollYProgress, [0, 1], [speed * 300, -speed * 300]);
return (
<motion.div
style={{ y }}
className={`absolute inset-0 will-change-transform ${className}`}
>
{children}
</motion.div>
);
}
export default ParallaxLayers;