Scroll

Horizontal scroll

Horizontal scroll is a scroll-driven layout where a full-viewport section is pinned in place and its inner track slides sideways as the user scrolls vertically. In GSAP you build it by animating the track's `x` with `ease: "none"`, pinning the section, and passing that tween as `containerAnimation` to any nested effect so children can fire off horizontal position instead of page scroll.

Updated July 1, 2026

Mechanics

How horizontal scroll works

The trick is that the page still scrolls vertically. You pin a viewport-sized section, then bind a horizontal tween to that pin's scroll range. As the user scrolls down, the pinned section holds and the inner track translates left. Nothing about the native scroll direction changes, you're just remapping vertical distance onto horizontal movement.

Animate the track's x (or xPercent), not the pinned element itself. Pin the parent wrapper and move the child, otherwise ScrollTrigger fights the transform it's using to pin. The tween MUST use ease: "none", or scroll position and horizontal position drift apart and the section feels like it's lagging behind your finger.

horizontal-scroll.js

To animate something WITHIN the horizontal track (a panel that fades in as it reaches center, a caption that lifts), give its ScrollTrigger containerAnimation: scrollTween. Now start: "left center" reads against horizontal position, not page scroll.

script.js

containerAnimation drops pin and snap

A ScrollTrigger that uses containerAnimation cannot itself pin or snap. Those two options only work against the real scroller. So the outer horizontal tween owns the pin, and the inner triggers only fire callbacks or scrub. Reach for snap on the outer tween's ScrollTrigger if you want panels to click into place.
When

Use horizontal scroll for

  • A pinned gallery or case-study strip that walks through panels one at a time
  • Editorial storytelling where each horizontal step is a chapter (product tours, timelines)
  • A wide dataset or roadmap that reads left-to-right but you don't want a nested scrollbar
  • Portfolio work reels where the horizontal motion is part of the feel
Alternatives

Use something else when

  • Content loops forever with no start or end, use a continuous marquee tween on xPercent, not a scroll-pinned track
  • You want a real swipeable carousel, use Draggable with inertia so touch drives it directly
  • The section is short and doesn't need pinning, a native overflow-x: auto track with scroll-snap-type: x is lighter and works with no JS
  • You only need the CSS-native version, scroll-driven animation-timeline: scroll(inline) covers simple cases, though support only reached roughly 85% once Safari 26 shipped it in September 2025 (Firefox still behind a flag, caniuse 2025), so cross-browser sites still lean on GSAP
In production

Both of these move a track sideways, but they drive it with a loop or drag rather than vertical scroll, so they're the horizontal-motion cousins of the pinned pattern above:

  • The Infinite Parallax Slider runs a seamless horizontal loop with layers moving at different speeds for depth, auto-playing and pausing on hover instead of binding to scroll
  • The Marquee scrolls content horizontally on a continuous loop and lets you drag to scrub, then releases with inertia, the interactive take on left-to-right motion

Reuse the width math

Wrap the x and end values in functions and set invalidateOnRefresh: true so the track re-measures after fonts load or the viewport resizes. A hardcoded pixel width is the most common reason a horizontal section overshoots or stops short on a different screen.
Used in components

See it running in production

FAQ

Common questions

Why does my horizontal scroll feel laggy or out of sync?
Almost always because the horizontal tween uses an ease other than none. GSAP eases warp progress across the range, so at any given scroll position the track is ahead of or behind where your finger is. Set ease: "none" on the tween that moves the track. Keep any easing feel for the nested child animations, not the container tween itself.
Can I pin AND use containerAnimation on the same ScrollTrigger?
No. A ScrollTrigger that references containerAnimation cannot pin or snap, those only work against the real scroller. The structure is: the outer tween that moves the track owns pin and scrub, and the inner triggers pass containerAnimation to fire against horizontal position. They're two separate layers.
How do I make panels snap into place?
Put snap on the outer horizontal tween's ScrollTrigger (the one that owns the pin), not on the nested containerAnimation triggers. snap: 1 / (panelCount - 1) snaps to each panel's progress value. You can also pass a snap object with duration and ease to control how it settles.
Why does the section break or overshoot after a resize?
The track width changed but the tween cached the old numbers. Make x and end functions that read track.scrollWidth and window.innerWidth at refresh time, and set invalidateOnRefresh: true. ScrollTrigger re-runs those functions on refresh (resize is auto-debounced 200ms), so the math stays correct across breakpoints.
Should I use GSAP or native CSS scroll-driven animation for this?
For a simple case where content just moves as you scroll, CSS animation-timeline: scroll(inline) works with zero JS. But cross-browser support only reached about 85% after Safari 26 in September 2025 and Firefox is still behind a flag, and the pinned section-with-nested-triggers pattern is far easier to orchestrate in ScrollTrigger. Production sites that need the pinned choreography still reach for GSAP.