GSAP Pinned Gallery Snap — Free ScrollTrigger Horizontal Snap

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain how pinning a section and scrubbing a horizontal tween off vertical scroll distance combine to make wheel input feel like sideways navigation, and why the end value and x target are computed as functions rather than fixed pixel numbers. It can also help you extend the pattern — ask for a version with visible progress dots that highlight the active panel, a vertical-panel variant, or a way to jump directly to a given panel via a nav click while keeping the snap and pin intact. Use it to make sure you understand the scrub/snap relationship before adapting the timing for your own content.

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:

text
Build a "pinned horizontal gallery with scroll snap" in plain HTML, CSS, and JavaScript using GSAP with its ScrollTrigger plugin (load both from a CDN).

Requirements:
- An intro section, a pinned gallery section containing a flex track of several full-viewport-width panels laid out horizontally, and an outro section that resumes normal vertical scrolling after the gallery.
- Use ScrollTrigger's pin: true on the gallery section so it stays fixed in the viewport for the duration of the horizontal scroll, and scrub (a small numeric value, not just true) to tie the horizontal tween's progress to vertical scroll position with slight smoothing.
- The horizontal tween's target x offset and the ScrollTrigger's end value must both be computed dynamically from the track's actual scrollWidth and the viewport width (as functions, not hardcoded numbers), so the scroll distance always matches exactly how far the track needs to move to reveal the final panel.
- Configure ScrollTrigger's snap option so that once the user stops scrolling, the gallery animates to rest on the nearest whole panel rather than stopping at an arbitrary point between two panels — derive the snap increment from the number of panels, not a hardcoded fraction.
- Set invalidateOnRefresh: true so a browser resize correctly recalculates the pin distance and horizontal target instead of using stale measurements.
- Confirm that scrolling past the last panel releases the pin and returns to normal vertical scrolling into the outro section.

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

  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

Portfolio galleries
A snapping alternative to scroll horizontal pin.
Product showcases
Step through products like scroll pin steps.
Case study sections
Pair with a scroll reveal grid intro.
Editorial features
Long-form storytelling with a paced panel-by-panel reveal.
Brand story pages
Combine with scroll svg path draw accents.
Onboarding tours
Snap through feature panels one screen at a time.

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.