webdevpuneet.com

GSAP Pinned Gallery Snap — Free ScrollTrigger Horizontal Snap

GSAP ScrollTrigger Pinned Gallery Snap · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="pg-intro"><h1>Pinned gallery</h1><p>Scroll down — the page pins while you scroll sideways through the panels, snapping to each one.</p></section>
<section class="pg-pin" id="pgPin">
  <div class="pg-track" id="pgTrack">
    <article class="pg-panel"><span class="pg-idx">01</span><h2>Horizon</h2></article>
    <article class="pg-panel"><span class="pg-idx">02</span><h2>Monolith</h2></article>
    <article class="pg-panel"><span class="pg-idx">03</span><h2>Cascade</h2></article>
    <article class="pg-panel"><span class="pg-idx">04</span><h2>Signal</h2></article>
    <article class="pg-panel"><span class="pg-idx">05</span><h2>Basin</h2></article>
  </div>
</section>
<section class="pg-outro"><p>Back to a normal vertical scroll.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Vertical-to-horizontal scroll
Wheel/touch input drives a sideways track.
True pinning
Section stays fixed for the whole gallery duration.
Panel snapping
Settles on the nearest whole panel after scrolling.
Scrubbed motion
scrub: 1 ties progress directly to scroll with slight ease.
Dynamic distance
end is computed from the track's real overflow width.
Resize-safe
invalidateOnRefresh recalculates on viewport change.
No extra markup
Snap math derives from panel count, not hardcoded steps.
Smooth release
Scrolling past the last panel resumes normal flow.

About this UI Snippet

GSAP ScrollTrigger Pinned Gallery Snap — Vertical Scroll, Horizontal Snap

Screenshot of the GSAP ScrollTrigger Pinned Gallery Snap snippet rendered live

This snippet turns ordinary vertical scrolling into a pinned, horizontally-snapping gallery — the section locks in place while your scroll input drives a track of panels sideways, and lifting your finger or wheel settles the view on the nearest whole panel instead of leaving it mid-transition. It's built entirely with GSAP and ScrollTrigger from a CDN, no scroll-jacking libraries required.

Pinning holds the section still

pin: true on the ScrollTrigger fixes #pgPin in the viewport once its top reaches the top of the screen, and keeps it fixed for the entire duration of the scroll range defined by end. Visually the page appears to stop scrolling vertically — but scroll input is still being captured and converted into progress along the tween.

Scroll distance becomes horizontal motion

The core tween is gsap.to(track, { x: () => -(track.scrollWidth - window.innerWidth), scrollTrigger: { scrub: 1, ... } }). scrub: 1 ties the tween's progress directly to scroll position (with a slight 1-second catch-up smoothing), so scrolling down moves the track left in lockstep — scroll up and it reverses. The end value is calculated from the track's actual overflow width, so it always matches exactly how far the track needs to travel to reveal the last panel.

Snap locks onto whole panels

Without snap, releasing the scroll mid-panel would leave the gallery awkwardly between two images. The snap: { snapTo: 1 / (panels - 1), duration: 0.4, ease: 'power1.inOut' } config divides the scroll progress into even increments — one per panel — and animates to the nearest one once scrolling settles, giving the gallery a deliberate, page-like feel instead of a loose scrub.

Responsive by recalculation

Both the x and end values are functions, not fixed numbers, so ScrollTrigger recalculates them on refresh. invalidateOnRefresh: true ensures a browser resize (which changes window.innerWidth and possibly track.scrollWidth) recomputes the whole scroll distance rather than reusing stale measurements.

Customizing it

Add more panels, change scrub to a larger number for looser lag, or drop snap entirely for a free-scrub gallery. Pair it with a scroll horizontal pin or scroll pin steps layout for related pinned-scroll patterns.

Step by step

How to Use

  1. 1
    Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSAn intro, pinned gallery, and outro section render.
  3. 3
    Scroll into the galleryThe section pins and panels slide horizontally.
  4. 4
    Pause mid-scrollThe view snaps to the nearest full panel.
  5. 5
    Keep scrolling past the last panelThe pin releases and vertical scroll resumes.
  6. 6
    Resize the windowScrollTrigger recalculates the pin and scroll distance.

Real-world uses

Common Use Cases

Snapping portfolio galleries
Pin a section and drive a horizontal track from vertical scroll, settling on the nearest whole panel when scrolling stops.
Product showcase steps
Step through products like scroll pin steps, with scrub: 1 adding slight ease to the connection with scroll.
Case study introductions
Pair with a scroll reveal grid intro, then lock into a snapping gallery for the main work.
Editorial feature stories
Pace a long-form story with panels that settle one screen at a time, as a snapping alternative to the free-flowing scroll horizontal pin.
Brand story pages
Combine with scroll SVG path draw accents, so a drawn line and a snapping gallery both follow scroll.
Related: GSAP Scroll Text Scramble
See the GSAP Scroll Text Scramble for a related scroll pattern worth pairing with this one.
Related: Scroll-Snap Carousel (view-timeline Scale)
See the Scroll-Snap Carousel (view-timeline Scale) for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The ScrollTrigger doesn't change scroll direction itself — it pins the section and uses the vertical scroll distance as the driver for a scrubbed gsap.to(track, { x: ... }) tween. As you scroll down within the pinned range, the tween's progress advances and the track's x transform moves it left, so scroll input reads as horizontal motion visually.

track.scrollWidth and window.innerWidth can change — on resize, font load, or content changes — so hardcoding a pixel value would drift out of sync with the actual track width. Passing functions lets ScrollTrigger call them fresh each time it recalculates, and invalidateOnRefresh forces that recalculation on resize.

snap: { snapTo: 1 / (panels - 1) } divides the scroll progress (0 to 1) into even increments, one per panel gap. When scrolling settles at a progress value that isn't exactly on one of those increments, GSAP animates to the nearest one over the given duration and easing, so the gallery always comes to rest on a full panel.

scrub: true ties the tween's progress to the scrollbar with zero lag, which can feel abrupt on fast wheel scrolls. Passing a number like 1 tells GSAP to take that many seconds to catch up to the scroll position, smoothing out jittery input while still feeling responsive.

Register ScrollTrigger and create the pinned tween inside a mount effect, scoping selectors to refs for the pin and track elements. Store the ScrollTrigger instance (or use gsap.context) and kill/revert it in the cleanup function so pins don't stack up across route changes or re-renders.