parallax /
CSS Perspective Parallax
Section is its own scroll container with perspective. Background lives on a far Z-plane so the browser scrolls it slower automatically. Zero JS.
Preview
Source
tsx
export interface ParallaxPerspective3dProps {
imageSrc?: string;
/** Mid-ground decoration on a closer Z-plane. Optional transparent image. */
midSrc?: string;
eyebrow?: string;
headline?: string;
subhead?: string;
}
export default function ParallaxPerspective3d({
imageSrc = "/heroes/landscape-card-bg.webp",
midSrc,
eyebrow = "Parallax · 08",
headline = "CSS perspective parallax",
subhead =
"The container has a 3D perspective; the image lives on a deeper Z-plane. Browser scrolls it slower automatically — zero JavaScript, butter-smooth on every device.",
}: ParallaxPerspective3dProps) {
return (
<section
className="relative isolate w-full overflow-y-auto overflow-x-hidden bg-slate-900"
style={{
height: "100svh",
perspective: "10px",
perspectiveOrigin: "0% 0%",
}}
>
{/* Sized to fill the scroll container's content area */}
<div className="relative" style={{ height: "180svh" }}>
{/* Back layer — pushed away on Z, scaled up to compensate. Browser scrolls it slower. */}
<div
className="absolute inset-0"
style={{
backgroundImage: `url(${imageSrc})`,
backgroundSize: "cover",
backgroundPosition: "center",
transform: "translateZ(-20px) scale(3)",
transformOrigin: "0% 0%",
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-black/20 to-black/70" />
{/* Mid layer — closer Z plane, optional */}
{midSrc && (
<div
className="absolute inset-x-0 bottom-0 h-1/2"
style={{
transform: "translateZ(-10px) scale(2)",
transformOrigin: "0% 100%",
backgroundImage: `url(${midSrc})`,
backgroundSize: "cover",
backgroundPosition: "bottom",
}}
/>
)}
{/* Foreground — Z=0 (default), scrolls normally */}
<div className="absolute inset-x-0 top-[60%] mx-auto max-w-3xl px-6 text-center text-white">
<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>
</div>
</section>
);
} Claude Code Instructions
CLI Install
npx innovations add perspective-3dWhere to use it
The 'old-school CSS trick' for parallax — a self-scrolling section with 3D perspective. Layers at translateZ(-Npx) scroll slower because the browser projects them through perspective.
KEY DETAIL: the section itself has overflow-y: auto — it scrolls INDEPENDENTLY from the page. This is its main limitation; embedding it inside a page that also scrolls produces nested scrolling. Best used as a FULL-PAGE parallax (set min-height: 100vh on body) or a discrete pinned section.
PROS:
- Zero JS
- Smoother than scroll-listener parallax (browser-native compositing)
- Multiple Z-planes compose naturally — add more layers with smaller |translateZ| for less travel
CONS:
- Nested scrolling can confuse touch users
- Doesn't compose well inside other scroll containers
- Z-math (translateZ depth × scale factor must match perspective) is finicky — if you change perspective from 10px, recompute the scale to compensate
USE: only when you can dedicate the whole viewport to it (landing-page hero, full-page scrollytelling).