parallax /
Layered Depth (3 speeds)
Three parallax planes — sky, silhouette, foreground — each at a different scroll speed. The cinematic
Preview
Source
tsx
"use client";
import { useEffect, useRef } from "react";
export interface ParallaxLayeredDepthProps {
/** Background sky/landscape image — moves slowest. */
backSrc?: string;
/** Mid-ground silhouette (transparent PNG/SVG ideal). Moves at mid speed. */
midSrc?: string;
eyebrow?: string;
headline?: string;
subhead?: string;
}
export default function ParallaxLayeredDepth({
backSrc = "/heroes/landscape-card-bg.webp",
midSrc,
eyebrow = "Parallax · 03",
headline = "Layered depth",
subhead =
"Three layers, three speeds. The deepest layer barely moves; the foreground keeps pace with scroll. Creates a sense of physical depth.",
}: ParallaxLayeredDepthProps) {
const sectionRef = useRef<HTMLElement | null>(null);
const backRef = useRef<HTMLDivElement | null>(null);
const midRef = useRef<HTMLDivElement | null>(null);
const fgRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
let raf = 0;
let visible = false;
const io = new IntersectionObserver(
(entries) => {
visible = entries[0]?.isIntersecting ?? false;
},
{ threshold: 0 }
);
if (sectionRef.current) io.observe(sectionRef.current);
const onScroll = () => {
if (!visible || !sectionRef.current) return;
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
const top = sectionRef.current!.getBoundingClientRect().top;
if (backRef.current) backRef.current.style.transform = `translate3d(0, ${-top * 0.15}px, 0)`;
if (midRef.current) midRef.current.style.transform = `translate3d(0, ${-top * 0.4}px, 0)`;
if (fgRef.current) fgRef.current.style.transform = `translate3d(0, ${-top * 0.7}px, 0)`;
});
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
io.disconnect();
window.removeEventListener("scroll", onScroll);
cancelAnimationFrame(raf);
};
}, []);
return (
<section
ref={sectionRef}
className="relative isolate flex min-h-[130svh] w-full items-center justify-center overflow-hidden bg-slate-900"
>
{/* Back layer — landscape image, slowest */}
<div
ref={backRef}
className="absolute inset-x-0 top-0 -z-30 h-[160%] will-change-transform"
style={{
backgroundImage: `url(${backSrc})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-slate-900/30 via-slate-900/40 to-slate-900/80" />
{/* Mid layer — silhouetted shapes drawn with SVG so the demo is self-contained.
Pass midSrc to override with your own transparent PNG. */}
<div
ref={midRef}
className="absolute inset-x-0 bottom-0 -z-10 h-[60%] will-change-transform"
>
{midSrc ? (
<img src={midSrc} alt="" className="h-full w-full object-cover object-bottom" />
) : (
<svg viewBox="0 0 1600 600" preserveAspectRatio="none" className="h-full w-full">
<path
d="M0 600 L0 400 Q 100 350 200 380 T 400 360 Q 500 320 600 360 T 800 340 Q 900 300 1000 340 T 1200 320 Q 1300 290 1400 320 T 1600 310 L1600 600 Z"
fill="#0f172a"
opacity="0.85"
/>
<path
d="M0 600 L0 500 Q 150 460 280 490 T 540 470 Q 660 440 820 470 T 1080 460 Q 1230 430 1380 460 T 1600 450 L1600 600 Z"
fill="#020617"
/>
</svg>
)}
</div>
{/* Foreground text — moves fastest */}
<div
ref={fgRef}
className="relative z-10 mx-auto max-w-3xl px-6 text-center text-white will-change-transform"
>
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.3em] text-white/70">
{eyebrow}
</p>
<h1 className="text-balance text-4xl font-semibold leading-[1.1] tracking-tight sm:text-6xl">
{headline}
</h1>
<p className="mx-auto mt-6 max-w-xl text-base leading-relaxed text-white/80 sm:text-lg">
{subhead}
</p>
</div>
</section>
);
} Claude Code Instructions
CLI Install
npx innovations add layered-depthWhere to use it
Three layers, three scroll speeds. Sky barely moves (0.15), silhouette moves more (0.4), foreground text moves fastest (0.7). Creates a strong sense of depth — like looking out the window of a moving train.
The mid-layer is an inline SVG silhouette by default so the demo ships with one file. Replace with a transparent PNG via midSrc for a real photo silhouette (mountains, skyline, treeline).
TIPS:
- Each layer needs to be TALLER than the section, or you'll see edges. Back is 160%, mid is 60% (anchored to bottom).
- For best depth perception, the mid layer should be visually distinct from the back (silhouette, treeline, building outline).
- Reduce all three speed coefficients (e.g. 0.05 / 0.15 / 0.3) for a subtler effect; increase for a more dramatic ride.
PERFORMANCE: 3 transforms per scroll frame instead of 1, but still cheap (3 GPU layers, all translate3d). IntersectionObserver-gated so it costs nothing off-screen.