Parallax Columns
Three image columns that move at different speeds as you scroll.
Scroll to see each column move at its own speed ↓
components/ui/parallax-columns.tsx
"use client";
import { motion, useScroll, useTransform } from "motion/react";
import { useRef, type RefObject } from "react";
const palette = [
"from-sky-200 to-blue-400",
"from-violet-200 to-indigo-400",
"from-rose-200 to-pink-400",
"from-amber-200 to-orange-400",
"from-emerald-200 to-teal-400",
"from-zinc-200 to-zinc-400",
];
type ParallaxColumnsProps = {
/** Number of tiles per column */
rows?: number;
container?: RefObject<HTMLElement | null>;
};
export function ParallaxColumns({ rows = 5, container }: ParallaxColumnsProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start end", "end start"] });
const speeds = [
useTransform(scrollYProgress, [0, 1], [80, -160]),
useTransform(scrollYProgress, [0, 1], [-60, 140]),
useTransform(scrollYProgress, [0, 1], [120, -220]),
];
return (
<div ref={ref} className="grid grid-cols-3 gap-4 overflow-hidden py-10">
{speeds.map((y, col) => (
<motion.div key={col} style={{ y }} className="flex flex-col gap-4">
{Array.from({ length: rows }).map((_, i) => {
const tone = palette[(col * 2 + i) % palette.length];
return (
<div
key={i}
className={`relative overflow-hidden rounded-2xl bg-gradient-to-br shadow-sm ${tone} ${i % 2 ? "aspect-[4/5]" : "aspect-square"}`}
>
<div className="absolute bottom-3 left-3 h-2 w-12 rounded-full bg-white/70" />
</div>
);
})}
</motion.div>
))}
</div>
);
}
export default function Demo() {
const ref = useRef<HTMLDivElement>(null);
return (
<div ref={ref} className="h-[420px] w-full max-w-2xl overflow-y-auto rounded-2xl border border-zinc-200 bg-white px-6">
<div className="flex h-32 items-center justify-center text-sm text-zinc-400">Scroll to see each column move at its own speed ↓</div>
<ParallaxColumns container={ref} />
<div className="h-32" />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/parallax-columns.tsxcomponents/ui/parallax-columns.tsx"use client"; import { motion, useScroll, useTransform } from "motion/react"; import { useRef, type RefObject } from "react"; const palette = [ "from-sky-200 to-blue-400", "from-violet-200 to-indigo-400", "from-rose-200 to-pink-400", "from-amber-200 to-orange-400", "from-emerald-200 to-teal-400", "from-zinc-200 to-zinc-400", ]; type ParallaxColumnsProps = { /** Number of tiles per column */ rows?: number; container?: RefObject<HTMLElement | null>; }; export function ParallaxColumns({ rows = 5, container }: ParallaxColumnsProps) { const ref = useRef<HTMLDivElement>(null); const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start end", "end start"] }); const speeds = [ useTransform(scrollYProgress, [0, 1], [80, -160]), useTransform(scrollYProgress, [0, 1], [-60, 140]), useTransform(scrollYProgress, [0, 1], [120, -220]), ]; return ( <div ref={ref} className="grid grid-cols-3 gap-4 overflow-hidden py-10"> {speeds.map((y, col) => ( <motion.div key={col} style={{ y }} className="flex flex-col gap-4"> {Array.from({ length: rows }).map((_, i) => { const tone = palette[(col * 2 + i) % palette.length]; return ( <div key={i} className={`relative overflow-hidden rounded-2xl bg-gradient-to-br shadow-sm ${tone} ${i % 2 ? "aspect-[4/5]" : "aspect-square"}`} > <div className="absolute bottom-3 left-3 h-2 w-12 rounded-full bg-white/70" /> </div> ); })} </motion.div> ))} </div> ); } export default function Demo() { const ref = useRef<HTMLDivElement>(null); return ( <div ref={ref} className="h-[420px] w-full max-w-2xl overflow-y-auto rounded-2xl border border-zinc-200 bg-white px-6"> <div className="flex h-32 items-center justify-center text-sm text-zinc-400">Scroll to see each column move at its own speed ↓</div> <ParallaxColumns container={ref} /> <div className="h-32" /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.