Horizontal Scroll Tutorial

This is the pattern I use for cinematic horizontal sections: a scrollable container, a track that moves sideways with GSAP, and a small wheel handler that turns vertical input into horizontal movement.

Live Demo

Hover the block and scroll. The section converts your wheel movement into a horizontal track animation.

Panel 1

Intro

Pin the section, move the track with GSAP, and let the scroll distance drive the whole sequence.

GSAPScrollTrigger
Panel 2

Concept

Pin the section, move the track with GSAP, and let the scroll distance drive the whole sequence.

GSAPScrollTrigger
Panel 3

Build

Pin the section, move the track with GSAP, and let the scroll distance drive the whole sequence.

GSAPScrollTrigger
Panel 4

Motion

Pin the section, move the track with GSAP, and let the scroll distance drive the whole sequence.

GSAPScrollTrigger
Panel 5

Polish

Pin the section, move the track with GSAP, and let the scroll distance drive the whole sequence.

GSAPScrollTrigger
Panel 6

Launch

Pin the section, move the track with GSAP, and let the scroll distance drive the whole sequence.

GSAPScrollTrigger

1) Register ScrollTrigger

Start by bringing in GSAP, the React hook, and ScrollTrigger so the animation can stay in sync with scroll position.

1import gsap from "gsap"
2import { ScrollTrigger } from "gsap/ScrollTrigger"
3import { useGSAP } from "@gsap/react"
4
5gsap.registerPlugin(ScrollTrigger)

2) Build a Horizontal Track

The core structure is just an overflow container plus a flex track. Each panel gets a fixed minimum width so GSAP has real distance to travel.

1<div ref={containerRef} className="overflow-x-auto overflow-y-hidden">
2  <div ref={trackRef} className="flex h-full items-center gap-4 px-6">
3    {panels.map((panel) => (
4      <article className="tutorial-horizontal-scroll-section min-w-[22rem]">
5        {panel.title}
6      </article>
7    ))}
8  </div>
9</div>

3) Animate the Track

The tween translates the whole track by the difference between its full width and the visible container width. ScrollTrigger handles the scrubbing for you.

1useGSAP(() => {
2  const container = containerRef.current
3  const track = trackRef.current
4  if (!container || !track) return
5
6  const tween = gsap.to(track, {
7    x: () => -(track.scrollWidth - container.clientWidth),
8    ease: "none",
9    scrollTrigger: {
10      trigger: track,
11      scroller: container,
12      horizontal: true,
13      start: "left left",
14      end: () => track.scrollWidth - container.clientWidth,
15      scrub: 0.6,
16    },
17  })
18
19  return () => {
20    tween.scrollTrigger?.kill()
21    tween.kill()
22  }
23})

4) Map Wheel Input to Horizontal Scroll

Without this part, trackpads feel good but normal mouse wheels feel awkward. Intercepting `deltaY` and mapping it to `scrollLeft` makes the interaction feel much more natural.

1const handleWheel = (event: WheelEvent) => {
2  if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
3    event.preventDefault()
4    container.scrollLeft += event.deltaY
5  }
6}
7
8container.addEventListener("wheel", handleWheel, { passive: false })