Innovations

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-depth

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