Parallax Columns

Three image columns that move at different speeds as you scroll.

Scroll to see each column move at its own speed ↓

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/parallax-columns.tsx

    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>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.