Innovations

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-3d

Where 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).