Horizontal Scroll

Vertical scrolling drives a row of cards sideways in a pinned section.

Scroll down — cards move sideways ↓
01

Discover

Research what users need

02

Define

Frame the right problem

03

Design

Explore many directions

04

Build

Ship real, working code

05

Measure

Learn from every release

06

Repeat

Small loops, big outcomes

End of section

Installation

  1. Install the dependencies

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

    components/ui/horizontal-scroll.tsx
    "use client";
    
    import { motion, useScroll, useTransform } from "motion/react";
    import { useEffect, useRef, type ReactNode, type RefObject } from "react";
    
    type HorizontalScrollProps = {
      children: ReactNode;
      /** Total scroll distance of the section, e.g. "300vh" or 1400 */
      height?: string | number;
      /** Height of the pinned viewport */
      stickyClassName?: string;
      container?: RefObject<HTMLElement | null>;
    };
    
    export function HorizontalScroll({ children, height = "300vh", stickyClassName = "h-screen", container }: HorizontalScrollProps) {
      const section = useRef<HTMLElement>(null);
      const viewport = useRef<HTMLDivElement>(null);
      const track = useRef<HTMLDivElement>(null);
      const distance = useRef(0);
      const { scrollYProgress } = useScroll({ target: section, container, offset: ["start start", "end end"] });
      const x = useTransform(scrollYProgress, (v) => -v * distance.current);
    
      useEffect(() => {
        const measure = () => {
          if (track.current && viewport.current) {
            distance.current = Math.max(0, track.current.scrollWidth - viewport.current.clientWidth);
          }
        };
        measure();
        const ro = new ResizeObserver(measure);
        if (track.current) ro.observe(track.current);
        if (viewport.current) ro.observe(viewport.current);
        return () => ro.disconnect();
      }, []);
    
      return (
        <section ref={section} style={{ height }} className="relative">
          <div ref={viewport} className={`sticky top-0 flex items-center overflow-hidden ${stickyClassName}`}>
            <motion.div ref={track} style={{ x }} className="flex w-max gap-5 px-8">
              {children}
            </motion.div>
          </div>
        </section>
      );
    }
    
    const slides = [
      { title: "Discover", text: "Research what users need", color: "from-sky-400 to-blue-600" },
      { title: "Define", text: "Frame the right problem", color: "from-violet-400 to-indigo-600" },
      { title: "Design", text: "Explore many directions", color: "from-pink-400 to-rose-600" },
      { title: "Build", text: "Ship real, working code", color: "from-amber-400 to-orange-600" },
      { title: "Measure", text: "Learn from every release", color: "from-emerald-400 to-teal-600" },
      { title: "Repeat", text: "Small loops, big outcomes", color: "from-zinc-600 to-zinc-900" },
    ];
    
    export default function Demo() {
      const ref = useRef<HTMLDivElement>(null);
      return (
        <div ref={ref} className="h-[400px] w-full max-w-4xl overflow-y-auto rounded-2xl border border-zinc-200 bg-white">
          <div className="flex h-24 items-center justify-center text-sm text-zinc-400">Scroll down — cards move sideways ↓</div>
          <HorizontalScroll container={ref} height={1500} stickyClassName="h-[400px]">
            {slides.map((s, i) => (
              <div key={s.title} className={`flex h-64 w-72 shrink-0 flex-col justify-between rounded-3xl bg-gradient-to-br p-7 text-white shadow-xl ${s.color}`}>
                <span className="text-sm font-medium opacity-80">0{i + 1}</span>
                <div>
                  <h3 className="text-3xl font-semibold tracking-tight">{s.title}</h3>
                  <p className="mt-1 text-white/80">{s.text}</p>
                </div>
              </div>
            ))}
          </HorizontalScroll>
          <div className="flex h-40 items-center justify-center text-sm text-zinc-400">End of section</div>
        </div>
      );
    }
    
  3. Use it

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