More Scroll Snippets
Scroll Horizontal Pin — Free GSAP ScrollTrigger Snippet
Scroll Horizontal Pin · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Horizontal Pin — Turn Vertical Scroll Into Sideways Travel

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.
Step by step
How to Use
- 1Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
- 2Paste HTML, CSS, and JSAn intro, a pinned track, and an outro render.
- 3Scroll downThe section pins and panels slide sideways.
- 4Scroll back upThe panels reverse — motion is scrubbed.
- 5Reach the endThe page releases to normal vertical scroll.
- 6Add a panelDrop in another .hp-panel; widths recalc.
Real-world uses
Common Use Cases
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.