parallax /
Reverse Parallax
The image moves in the opposite direction of the scroll. Counter-intuitive and memorable. Use sparingly.
Preview
Source
tsx
"use client";
import { useEffect, useRef } from "react";
export interface ParallaxReverseProps {
imageSrc?: string;
/** How fast the image moves AGAINST the scroll. 0 = pinned, 1 = matches scroll speed in opposite direction. */
speed?: number;
eyebrow?: string;
headline?: string;
subhead?: string;
}
export default function ParallaxReverse({
imageSrc = "/heroes/crouchingtiger/bg.webp",
speed = 0.45,
eyebrow = "Parallax · 11",
headline = "Reverse parallax",
subhead =
"The image moves in the OPPOSITE direction of the scroll. Scroll down → image moves down. Counter-intuitive, unsettling, memorable.",
}: ParallaxReverseProps) {
const sectionRef = useRef<HTMLElement | null>(null);
const imgRef = 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 || !imgRef.current) return;
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
const top = sectionRef.current!.getBoundingClientRect().top;
// POSITIVE offset = move DOWN when section scrolls UP. Reverse of translate-y.
const offset = top * speed;
imgRef.current!.style.transform = `translate3d(0, ${offset}px, 0)`;
});
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
io.disconnect();
window.removeEventListener("scroll", onScroll);
cancelAnimationFrame(raf);
};
}, [speed]);
return (
<section
ref={sectionRef}
className="relative isolate flex min-h-[120svh] w-full items-center justify-center overflow-hidden bg-slate-900"
>
<div
ref={imgRef}
className="absolute inset-x-0 -top-[30%] -z-10 h-[160%] will-change-transform"
style={{
backgroundImage: `url(${imageSrc})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
<div className="absolute inset-0 -z-0 bg-gradient-to-b from-black/40 via-black/20 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 reverseWhere to use it
Same engine as translate-y, but the offset sign is flipped — when the user scrolls DOWN, the image moves DOWN. Brain expects the opposite, which is exactly why it's memorable.
WHEN TO USE (sparingly):
- Splash / cover sections where unease is the point
- Art-directed editorial
- Section transitions to signal 'something different is happening'
WHEN NOT TO USE:
- Anywhere users scroll for a long time — they'll get queasy
- Information-dense pages
- Anything where flow matters more than impression
TUNING: speed 0.45 is the comfortable maximum; above 0.6 it actively fights the user. Start at 0.3 for subtlety.