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
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.
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.
containerAnimation drops pin and snap
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.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
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
Draggablewith inertia so touch drives it directly - The section is short and doesn't need pinning, a native
overflow-x: autotrack withscroll-snap-type: xis 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
Related Annnimate components
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
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.See it running in production
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. Setease: "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
containerAnimationcannot pin or snap, those only work against the real scroller. The structure is: the outer tween that moves the track ownspinandscrub, and the inner triggers passcontainerAnimationto fire against horizontal position. They're two separate layers. - How do I make panels snap into place?
- Put
snapon 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 withdurationandeaseto 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
xandendfunctions that readtrack.scrollWidthandwindow.innerWidthat refresh time, and setinvalidateOnRefresh: 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.
@gsap/react that scopes GSAP animations to a component and cleans them up automatically on unmount.NextinvalidateOnRefreshinvalidateOnRefresh is a ScrollTrigger option that tells GSAP to re-read each tween's starting values on every ScrollTrigger.refresh().