Back to Dictionary
Scroll & Navigation
●●○Customize
Context dependent

Horizontal Scroll Gallery

A section of the page where vertical scrolling temporarily translates into horizontal scrolling to reveal a gallery of cards or images, before continuing vertically.

Use

Showcasing features, portfolios, or timelines sequentially without taking up massive vertical space

Avoid

If the content contains its own vertical scrolling (e.g., long text boxes)

Safer Alternative

No safer default listed.

Risk Level

Low risk

Live Demo

Scroll Down ↓
Gallery Item 1Horizontal scroll
Gallery Item 2Horizontal scroll
Gallery Item 3Horizontal scroll
Gallery Item 4Horizontal scroll
Gallery Item 5Horizontal scroll
End of Gallery

What It Is

A section of the page where vertical scrolling temporarily translates into horizontal scrolling to reveal a gallery of cards or images, before continuing vertically.

When to Use

  • Showcasing features, portfolios, or timelines sequentially without taking up massive vertical space

When NOT to Use

  • If the content contains its own vertical scrolling (e.g., long text boxes)

Configuration Tips

  • 01Use a sticky container whose height is equal to the total width of the horizontal items, mapping window vertical scroll to the translateX of the gallery track

You've Seen It In

Awwwards WinnersApple Mac Pro page

AI Implementation Prompts

Move from a fast scaffold to production details, then tune timing and edge states.

Create a horizontal scroll section pinned to the viewport.

Reference Implementation

The same implementation approach used by the live preview, kept compact enough to inspect and adapt.

"use client";
import { useRef, useEffect } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';

export function HorizontalScrollGalleryDemo({ isPlaying = false }: { isPlaying?: boolean }) {
    const containerRef = useRef<HTMLDivElement>(null);
    const targetRef = useRef<HTMLDivElement>(null);

    const { scrollYProgress } = useScroll({
        container: containerRef,
        target: targetRef,
        offset: ["start start", "end end"]
    });

    const x = useTransform(scrollYProgress, [0, 1], ["0%", "-60%"]);

    useEffect(() => {
        if (!isPlaying || !containerRef.current) return;
        let pos = 0;
        let direction = 1;

        const interval = setInterval(() => {
            if (!containerRef.current) return;
            pos += 2 * direction;
            if (pos > 800) direction = -1;
            if (pos <= 0) direction = 1;
            containerRef.current.scrollTop = pos;
        }, 16);

        return () => clearInterval(interval);
    }, [isPlaying]);

    return (
        <div className="flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-white p-4 shadow-sm ring-1 ring-stone-200">
            <div
                ref={containerRef}
                className="w-full max-w-sm h-56 overflow-y-scroll rounded-xl shadow-inner border border-stone-200 bg-stone-50 hide-scrollbar relative"
                style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
            >
                <div className="h-[150px] flex items-center justify-center text-stone-400 font-bold uppercase tracking-widest text-[10px]">Scroll Down ↓</div>

                {"text-stone-400 italic">/* 
                  The trick for horizontal scroll: 
                  Container has a large height (e.g. 800px) so the user can scroll vertically for a long time.
                  Inside, there's a sticky element that holds the horizontal track.
                */}
                <div ref={targetRef} className="h-[1000px] relative">
                    <div className="sticky top-0 flex h-56 items-center overflow-hidden bg-stone-900 border-y border-stone-800">
                        <motion.div style={{ x }} className="flex gap-4 px-8 w-max">
                            {[1, 2, 3, 4, 5].map((card) => (
                                <div key={card} className="w-56 h-36 bg-stone-800 rounded-xl shrink-0 shadow-2xl border border-stone-700 flex flex-col justify-end p-4 relative overflow-hidden">
                                    <div className="absolute inset-0 bg-gradient-to-br from-indigo-500/20 to-purple-500/20 opacity-50 mix-blend-overlay" />
                                    <span className="text-white font-bold relative z-10">Gallery Item {card}</span>
                                    <span className="text-white/50 text-xs font-medium relative z-10">Horizontal scroll</span>
                                </div>
                            ))}
                        </motion.div>
                    </div>
                </div>

                <div className="h-[150px] flex items-center justify-center text-stone-400 font-bold uppercase tracking-widest text-[10px]">End of Gallery</div>
            </div>
        </div>
    );
}