Scroll Horizontal Pin — Free GSAP ScrollTrigger Snippet

Scroll Horizontal Pin · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

ScrollTrigger pin
Section sticks while panels travel.
Scrubbed motion
Panels track the scrollbar both ways.
Overflow math
Travel equals scrollWidth minus viewport.
Natural ratio
One screen scroll per panel via end.
Resize-safe
invalidateOnRefresh recomputes widths.
Per-panel parallax
Headings settle as panels cross center.
Linear mapping
ease none keeps scroll and position locked.
Any panel count
Flex max-content adapts to the markup.

About this UI Snippet

Scroll Horizontal Pin — Turn Vertical Scroll Into Sideways Travel

Screenshot of the Scroll Horizontal Pin snippet rendered live

The horizontal scroll pin is the signature interaction where a full-screen section sticks to the viewport and its panels slide sideways as you keep scrolling down — the showcase technique used by award-winning agency and product sites. This snippet builds it with GSAP and the ScrollTrigger plugin (both loaded from a CDN), plus plain HTML and CSS.

Pinning the section

ScrollTrigger's pin: true fixes the .hp-pin section in place while the page scroll continues to advance. During that pinned window, a single gsap.to tween moves the inner .hp-track along its x-axis from 0 to the negative of its overflow width (scrollWidth − innerWidth), so the row of 100vw panels travels exactly far enough to reveal the last one. The track is laid out with display: flex and width: max-content so it overflows horizontally rather than wrapping.

Scrub ties motion to the scrollbar

Setting scrub: 1 links the tween's progress to the scroll position with a one-second catch-up, so the panels move precisely as fast as you scroll — forward and backward — instead of playing on a fixed timeline. The end is computed as += the overflow distance, which makes one screen of vertical scrolling map to one panel of horizontal travel, the ratio that feels most natural. ease: 'none' keeps the mapping perfectly linear so there's no drift between scroll and position.

Responsive recalculation

Widths depend on the viewport, so invalidateOnRefresh: true tells ScrollTrigger to recompute the start, end, and tween values on resize or orientation change. Using function-based values for x and end means those numbers are re-read at refresh time rather than frozen at load — the key to a horizontal scroller that doesn't break when the window changes size.

Per-panel parallax

Each panel's heading gets a secondary tween driven by the same horizontal motion, easing in from an offset and a low opacity as the panel crosses center. This layered movement — the panel translating while its content settles — adds depth so the section reads as more than a flat conveyor belt.

Why GSAP for this

Horizontal-on-vertical scroll is awkward with native CSS: position: sticky can pin, but mapping scroll distance to a transform, keeping it reversible, and recalculating on resize is exactly what ScrollTrigger is built for. GSAP handles the pin spacer, the scrub math, and the refresh lifecycle, so the snippet stays compact and robust.

Customizing it

Add or remove panels — the overflow math adapts automatically — change the scrub smoothing, adjust the scroll-to-travel ratio via end, or restyle the panels. Pair it with a scroll pin steps section, a scroll gallery pin, or stacking scroll cards.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to work out the overflow math or the containerAnimation wiring by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the x tween and the end value are written as functions rather than fixed numbers, and how invalidateOnRefresh keeps that math correct across resizes. The same assistant can help optimize it — for instance checking whether the per-panel parallax tweens should be batched into one ScrollTrigger.batch call instead of one trigger per panel once the panel count grows large. It is just as useful for extending the effect: ask it to add snap points so panels settle rather than stop mid-slide, layer in a progress indicator for the current panel, or make the horizontal track itself draggable with a mouse or touch swipe alongside the scroll-driven motion. 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:

text
Build a "horizontal scroll pin" section in plain HTML, CSS, and JavaScript using GSAP and its ScrollTrigger plugin (load both from a CDN, no build step, no other libraries).

Requirements:
- A pinned section containing a flex row of full-viewport-width panels, laid out with display: flex and width: max-content so the row overflows horizontally instead of wrapping.
- Use ScrollTrigger's pin: true to stick the section in place while normal page scroll continues, and a single gsap.to tween that moves the row's x from 0 to the negative of its own overflow distance (scrollWidth minus the viewport width) so the last panel becomes reachable.
- The tween's x target and the ScrollTrigger's end value must both be functions (not fixed numbers) that compute the overflow distance at call time, and invalidateOnRefresh must be set to true, so the whole scroller recalculates correctly after a window resize or orientation change.
- Use scrub (a numeric value, not scrub: true) so the horizontal position tracks the scrollbar with a slight catch-up smoothing, with ease: none so the mapping between scroll distance and horizontal position stays linear.
- Add a secondary parallax tween per panel (for example on its heading) that uses containerAnimation pointing at the horizontal tween, not the page's own scroll, so that inner motion is keyed to the panel's position inside the horizontal track rather than to vertical page scroll.
- The section must fully reverse when the user scrolls back up, and must support any number of panels without code changes beyond adding markup.

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, a pinned track, and an outro render.
  3. 3
    Scroll downThe section pins and panels slide sideways.
  4. 4
    Scroll back upThe panels reverse — motion is scrubbed.
  5. 5
    Reach the endThe page releases to normal vertical scroll.
  6. 6
    Add a panelDrop in another .hp-panel; widths recalc.

Real-world uses

Common Use Cases

Process sections
Step through stages like scroll pin steps.
Galleries
A pinned scroll gallery pin variant.
Case studies
Showcase work beside stacking scroll cards.
Product tours
Pair with a scroll zoom hero.
Storytelling
Complement a scroll pin story.
Portfolios
Slide projects past a portfolio hero.
Related: Scroll Map Journey Story
See the Scroll Map Journey Story for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

ScrollTrigger pins the section with pin: true, and during that pinned window a gsap.to tween moves the inner track along x from 0 to negative its overflow width (scrollWidth minus innerWidth). So as the page scroll advances, the row of 100vw panels translates left exactly far enough to reveal the last one.

scrub: 1 links the tween's progress to the scrollbar with a one-second catch-up, so the panels move as fast as you scroll and reverse when you scroll back, rather than playing on a fixed timeline. Combined with ease: none, the scroll position and the horizontal offset stay locked together.

The x distance and the end are function-based values, and invalidateOnRefresh: true tells ScrollTrigger to re-read them on resize or orientation change. So the overflow width, pin length, and travel are recomputed for the new viewport instead of being frozen at load — without this, horizontal scrollers break when the window changes.

The end is set to +=(scrollWidth − innerWidth), which makes the pinned scroll distance equal the horizontal travel distance, so one viewport of vertical scrolling reveals roughly one panel. Increase the end value to slow the travel down, or decrease it to speed the panels up.

Register ScrollTrigger once, then create the tween inside a mount effect (useGSAP or useLayoutEffect in React, onMounted in Vue, ngAfterViewInit in Angular) scoped to refs for the section and track. Critically, return a cleanup that calls ScrollTrigger.getAll().forEach(t => t.kill()) or ctx.revert() so pins are torn down on unmount and route changes. The CSS ports unchanged.