# Horizontal scroll sections with GSAP ScrollTrigger

> Build a pinned horizontal scroll section with GSAP ScrollTrigger: pin, scrub, and a track that slides sideways as you scroll. Complete code included.

Published 2026-07-22 by Jake. Canonical: https://gsapvault.com/blog/horizontal-scroll-gsap

---
A horizontal scroll section pins to the viewport while normal vertical scrolling slides a row of panels sideways, then releases the page when the last panel arrives. It's a staple of portfolio and product sites, and ScrollTrigger handles all the hard parts. Here's a working basic version.

## What you'll build

A full-height section containing a horizontal track of panels. When the section reaches the top of the viewport it pins, scroll input moves the track sideways, and the pin releases once the track has fully traversed. The travel distance is measured from the track's real width, so it works with any number of panels and respects reduced motion preferences.

## Step 1: Add GSAP

Add GSAP and ScrollTrigger before your closing `</body>` tag:

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
```

## Step 2: Add the markup and CSS

The structure is a section wrapping a flex track of panels:

```html
<section class="hscroll">
  <div class="hscroll-track">
    <div class="hscroll-panel">Panel one</div>
    <div class="hscroll-panel">Panel two</div>
    <div class="hscroll-panel">Panel three</div>
    <div class="hscroll-panel">Panel four</div>
  </div>
</section>
```

```css
.hscroll {
  overflow: hidden;
}

.hscroll-track {
  display: flex;
  height: 100vh;
}

.hscroll-panel {
  flex: 0 0 100vw;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Reduced motion: stack the panels as a normal vertical page */
@media (prefers-reduced-motion: reduce) {
  .hscroll-track {
    flex-direction: column;
    height: auto;
  }

  .hscroll-panel {
    flex-basis: auto;
    min-height: 60vh;
  }
}
```

Panels don't have to be `100vw`. Any mix of widths works because the JavaScript measures the track rather than counting panels.

## Step 3: Add the effect code

```javascript
document.addEventListener('DOMContentLoaded', function () {
  // Reduced motion: the CSS fallback stacks panels vertically instead
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  gsap.registerPlugin(ScrollTrigger);

  const track = document.querySelector('.hscroll-track');

  const distance = function () {
    return track.scrollWidth - window.innerWidth;
  };

  gsap.to(track, {
    x: function () { return -distance(); },
    ease: 'none',
    scrollTrigger: {
      trigger: '.hscroll',
      start: 'top top',
      end: function () { return '+=' + distance(); },
      pin: true,
      scrub: 1,
      invalidateOnRefresh: true
    }
  });
});
```

Three details matter here. The tween's `x` and the trigger's `end` are functions, so combined with `invalidateOnRefresh` every resize re-measures the layout instead of animating to a stale distance. The `end` of `'+=' + distance()` means one pixel of vertical scroll equals one pixel of horizontal travel, which feels natural. And `ease: 'none'` keeps the track locked to the scrollbar; easing belongs on elements inside the panels, not on the scrub itself.

### Optional: snap to panels

If every panel is the same width, snapping is one line in the `scrollTrigger` config:

```javascript
snap: 1 / (track.children.length - 1)
```

## That's it

Scroll down and the section pins while the panels slide past. This basic version handles any number of equal-width panels, re-measures on resize, and falls back to a vertical layout for reduced motion users.

---

## Want more control?

The [Horizontal Scroll Section effect](/effects/horizontal-scroll-section) includes features this basic version doesn't have:

- **Variable-width panels** - snap points measured from each panel's real position, so mixed layouts snap correctly and survive resizes
- **Per-element parallax** - headline and image layers drift at their own speeds using containerAnimation ScrollTriggers
- **Progress bar** - a scrub-linked bar driven by a quickSetter, with zero tween churn
- **Speed control** - traverse the track faster or slower than 1:1 via a data attribute
- **Keyboard accessibility** - focusing a link inside an off-screen panel scrolls it into view

[View the full effect with all options →](/effects/horizontal-scroll-section)