Innovations

Split-band Shear

Image is sliced into N horizontal bands; each shears sideways at a different rate. Glitchy, editorial, film-strip parallax.

Preview

Source

tsx
"use client";

import { useEffect, useRef } from "react";

export interface ParallaxSplitBandProps {
  imageSrc?: string;
  /** Number of horizontal bands. 5 is the sweet spot — too many and the effect blurs. */
  bands?: number;
  /** Max horizontal shear in px per band. */
  shear?: number;
  eyebrow?: string;
  headline?: string;
  subhead?: string;
}

export default function ParallaxSplitBand({
  imageSrc = "/heroes/landscape-card-bg.webp",
  bands = 5,
  shear = 120,
  eyebrow = "Parallax · 09",
  headline = "Split-band shear",
  subhead =
    "The image is sliced into horizontal bands. Each band shears sideways at a different rate as you scroll — a glitchy, editorial 'film-strip' parallax.",
}: ParallaxSplitBandProps) {
  const sectionRef = useRef<HTMLElement | null>(null);
  const bandRefs = useRef<(HTMLDivElement | 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;
        const winH = window.innerHeight;
        const p = -top / winH; // 0 at section top hitting viewport top
        bandRefs.current.forEach((el, i) => {
          if (!el) return;
          // alternate direction by band index, scale by shear
          const direction = i % 2 === 0 ? 1 : -1;
          const intensity = ((i + 1) / bands) * shear * direction;
          el.style.transform = `translate3d(${p * intensity}px, 0, 0)`;
        });
      });
    };

    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => {
      io.disconnect();
      window.removeEventListener("scroll", onScroll);
      cancelAnimationFrame(raf);
    };
  }, [bands, shear]);

  const bandHeight = 100 / bands;

  return (
    <section
      ref={sectionRef}
      className="relative isolate flex min-h-[130svh] w-full items-center justify-center overflow-hidden bg-slate-900"
    >
      {/* Horizontal bands stacked vertically. Each is a div with background-image
          positioned so that the full image is visible across all bands together. */}
      <div className="absolute inset-0 -z-10">
        {Array.from({ length: bands }).map((_, i) => (
          <div
            key={i}
            ref={(el) => {
              bandRefs.current[i] = el;
            }}
            className="absolute left-0 w-[120%] -translate-x-[10%] will-change-transform"
            style={{
              top: `${i * bandHeight}%`,
              height: `${bandHeight}%`,
              backgroundImage: `url(${imageSrc})`,
              backgroundSize: "100% auto",
              backgroundPosition: `center ${-(i * 100) / (bands - 1)}%`,
              backgroundRepeat: "no-repeat",
            }}
          />
        ))}
      </div>
      <div className="absolute inset-0 -z-0 bg-gradient-to-b from-black/40 via-black/25 to-black/70" />
      <div className="relative z-10 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>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add split-band

Where to use it

Each band is the SAME image, positioned via background-position so it aligns vertically — but each band translates X at a different rate. Result: a unified image that shears apart on scroll. TUNING: - bands (default 5) — 3 = chunky, 5 = sweet spot, 8+ = noisy/blurry - shear (default 120px) — max horizontal travel per band. Subtle: 40. Editorial: 120. Aggressive: 240. - Bands alternate direction (left/right) for X-shape; remove the 'direction = i % 2 === 0 ? 1 : -1' if you want all bands shearing the same way. WHEN TO USE: - Editorial / fashion / art-direction sites that can wear weird - Section transitions ('something different begins here') - Avoid on dense informational hero — it pulls so much attention it overshadows the content The 120% width + -10% offset gives the bands room to shear without exposing the section background.