Scroll Gallery Pin — Free GSAP ScrollTrigger Snippet
Scroll Gallery Pin · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Gallery Pin — A Pinned Horizontal Gallery With Live Caption

Scroll gallery pin is the portfolio gallery where the section sticks to the screen and a row of images glides sideways as you scroll, with the heading and a counter updating to name whichever image is in focus — the polished showcase from agency and photography sites. This snippet builds it with GSAP and ScrollTrigger (from a CDN), plus plain HTML and CSS.
Pin and translate the rail
ScrollTrigger pins the gallery section and a single scrubbed tween moves the inner rail along its x-axis by its overflow distance (scrollWidth − innerWidth, plus a little padding so the last item clears the edge). So vertical scrolling drives the horizontal travel, and the section holds in place until the rail reaches its end — the standard horizontal-scroll-on-pin pattern, here applied to a captioned gallery.
A live caption and counter
What sets this apart from a plain horizontal scroller is the onUpdate callback. It reads self.progress (0 to 1), rounds it to the nearest item index, and writes that item's caption into the heading and updates a "03 / 05" counter. Because the index is rounded, the heading snaps to whichever image is most centered, giving the gallery a sense of discrete "slides" even though the motion is continuous. The counter uses tabular-nums so it doesn't jitter.
Responsive distance
The travel distance is computed in a function and invalidateOnRefresh: true re-runs it on resize, so the rail always scrolls exactly far enough regardless of viewport width — items are sized in vw so the gallery adapts, and the pin length (end) tracks the same function. This is what keeps a horizontal gallery from breaking or leaving dead scroll when the window changes.
Smooth catch-up
scrub: 1 gives the rail a one-second easing toward the scroll position, so the images glide with a touch of momentum rather than locking rigidly to the scrollbar — a more gallery-like feel. ease: 'none' on the tween keeps the underlying mapping linear while the scrub adds the smoothing.
Composited and clean
The rail moves with a transform (x), so it composites on the GPU with no reflow even with large image cards, and overflow: hidden on the pinned section clips the off-screen items. Captions sit over a gradient scrim so they stay legible on any image.
Customizing it
Swap the gradient figures for real images, change the item width or gap, add more items (the distance recalculates), or snap the rail to each item. Pair it with a scroll horizontal pin, a photo gallery, or a coverflow carousel.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the responsive distance math from scratch. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the travel distance function subtracts window.innerWidth from the rail's scrollWidth and adds 12% of padding, and why both the tween's x value and the ScrollTrigger's end are defined as functions rather than plain numbers. The same assistant is helpful for optimizing it — asking whether invalidateOnRefresh recalculating on every resize event is expensive with many high-resolution images, or whether scrub: 1's one-second easing needs tuning for a rail with far more items. It's also great for extending the effect: ask it to add clickable thumbnail dots synced to the current index, make each figure a link to a full case-study page, or add a subtle parallax on the figure backgrounds as the rail scrolls past them. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "scroll gallery pin" effect in plain HTML, CSS, and JavaScript using GSAP and its ScrollTrigger plugin (load both from a CDN, no build step).
Requirements:
- A pinned section containing a heading area (a title and an index counter like "01 / 05") and a horizontal rail of figure elements laid out with flexbox and no wrapping, each figure a fixed viewport-relative width with a caption.
- Compute the rail's horizontal travel distance as a function — scrollWidth minus window.innerWidth, plus roughly 12% of the viewport width so the last item fully clears the edge — and use that same function for both the tween's target x value and the ScrollTrigger's end value, so both stay in sync.
- Pin the section and scrub a single tween that moves the rail's x to the negative of that computed distance, using scrub: 1 (not scrub: true) so the rail glides with a bit of momentum rather than locking rigidly to the scrollbar.
- Set invalidateOnRefresh: true on the ScrollTrigger so the distance function is recalculated on window resize, keeping the horizontal travel correct at any viewport width without a hardcoded pixel value.
- In the tween's onUpdate callback, read self.progress (0 to 1), round it to the nearest figure index, and write that figure's caption text into the heading and update a zero-padded counter (e.g. "03 / 05") — the index must be rounded, not floored, so the heading feels centered on the most-visible figure.
- Keep the rail's motion to a transform (x) only, with overflow hidden on the pinned section clipping off-screen figures, and each caption legible over its figure via a bottom gradient scrim.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
- 2Paste HTML, CSS, and JSA pinned gallery with a heading renders.
- 3Scroll downThe image rail glides sideways.
- 4Watch the headingThe title and counter track the focused image.
- 5Resize the windowThe travel distance recalculates.
- 6Use real imagesSwap the gradient figures for photos.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
ScrollTrigger pins the section and a scrubbed tween moves the rail along x by its overflow distance (scrollWidth − innerWidth plus padding). Vertical scrolling drives the horizontal travel while the section holds in place until the rail ends — the horizontal-scroll-on-pin pattern, applied to a captioned image gallery.
The tween's onUpdate reads self.progress from 0 to 1, rounds it to the nearest item index, and writes that item's caption into the heading plus a 03 / 05 counter. Rounding makes the title snap to whichever image is most centered, giving discrete slide feedback even though the rail moves continuously.
Yes. The travel distance is a function and invalidateOnRefresh: true re-runs it on resize, so the rail scrolls exactly far enough at any width, and the pin length tracks the same function. Items are sized in vw so the gallery itself reflows, avoiding dead scroll or a cut-off last image.
scrub: 1 gives the rail a one-second easing toward the scroll position, so the images glide with a little momentum rather than locking rigidly to the scrollbar — a more gallery-like feel. ease: none keeps the underlying scroll-to-position mapping linear while the scrub value adds the smoothing on top.
In a mount effect, register ScrollTrigger and create the pinned, scrubbed tween scoped to refs for the section and rail, with onUpdate setting state (active index) or writing to refs for the caption and counter. Use function-based distance and invalidateOnRefresh. Return a cleanup that reverts the GSAP context. The CSS ports unchanged.