parallax /
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-bandWhere 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.