Innovations

Sticky-pin Storytelling

Image pins to the viewport with position:sticky while panel content scrolls past it. Pure CSS, perfect on iOS.

Preview

Source

tsx
export interface ParallaxStickyPinProps {
  imageSrc?: string;
  eyebrow?: string;
  panels?: { title: string; body: string }[];
}

export default function ParallaxStickyPin({
  imageSrc = "/heroes/sba/bg-2.jpg",
  eyebrow = "Parallax · 05",
  panels = [
    {
      title: "Sticky-pin parallax",
      body: "The background image pins to the viewport while the foreground panels scroll past it.",
    },
    {
      title: "Pure CSS, zero JS",
      body: "Just position: sticky on the image wrapper. Browser handles everything — buttery smooth, even on iOS.",
    },
    {
      title: "Use it for storytelling",
      body: "Each panel can reveal a new layer, a new statistic, a new chapter. The image acts as the stage.",
    },
  ],
}: ParallaxStickyPinProps) {
  return (
    <section className="relative isolate w-full bg-slate-900">
      <div className="relative">
        {/* Sticky image — pins to the viewport while the content scrolls past it */}
        <div className="sticky top-0 -z-10 h-screen w-full overflow-hidden">
          <div
            className="absolute inset-0"
            style={{
              backgroundImage: `url(${imageSrc})`,
              backgroundSize: "cover",
              backgroundPosition: "center",
            }}
          />
          <div className="absolute inset-0 bg-gradient-to-b from-black/40 via-black/30 to-black/70" />
        </div>

        {/* Panels — sit on top of the sticky image, scroll normally */}
        <div className="relative -mt-screen" style={{ marginTop: "-100vh" }}>
          <p className="mx-auto max-w-3xl px-6 pt-32 text-center text-xs font-semibold uppercase tracking-[0.3em] text-white/70">
            {eyebrow}
          </p>
          {panels.map((p, i) => (
            <div
              key={i}
              className="flex min-h-screen items-center justify-center px-6 py-24"
            >
              <div className="mx-auto max-w-2xl rounded-2xl bg-white/10 p-8 text-center text-white backdrop-blur-md ring-1 ring-white/15 sm:p-12">
                <p className="mb-2 text-xs font-semibold uppercase tracking-[0.25em] text-white/60">
                  Panel {String(i + 1).padStart(2, "0")}
                </p>
                <h2 className="text-balance text-3xl font-semibold leading-tight sm:text-4xl">
                  {p.title}
                </h2>
                <p className="mt-4 text-base leading-relaxed text-white/80 sm:text-lg">
                  {p.body}
                </p>
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add sticky-pin

Where to use it

position: sticky does all the work. Zero JS. Perfect cross-browser parity (including iOS Safari, unlike classic-fixed). The image wrapper is sticky with h-screen, the panels container has a negative top margin equal to one viewport so the first panel sits on top of the image. WHEN TO USE: - Long-form storytelling (case studies, product narratives, "how we built this" pages) - Each panel reveals a new piece while the visual stays present - Great with a video instead of an image (drop a <video autoPlay muted loop playsInline> in place of the bg div) EXTENDING: - Pass any number of panels (default 3). Section height auto-grows with each. - To swap the image partway through, layer multiple sticky-pin sections back-to-back. - For text-on-image styles, drop the backdrop-blur card and use plain text with a bottom-gradient.