# How to Implement the Horizontal-Pan Pattern with GSAP ScrollTrigger in taste-skill

> Learn to implement the horizontal-pan pattern with GSAP ScrollTrigger. Discover the smooth vertical scroll to horizontal animation and use the ready-to-use React component from taste-skill.

- Repository: [Leon Lin/taste-skill](https://github.com/Leonxlnx/taste-skill)
- Tags: how-to-guide
- Published: 2026-05-31

---

**The taste-skill repository ships a ready-to-use HorizontalPan component that creates a smooth horizontal-pan effect while the user scrolls vertically, implemented with GSAP ScrollTrigger and wrapped in a React client component.**

The **horizontal-pan pattern** transforms a standard vertical scroll into an immersive horizontal sliding experience. In the **Leonxlnx/taste-skill** project, this pattern is implemented as a reusable React component that leverages GSAP's ScrollTrigger plugin to pin the viewport and scrub through a horizontally overflowing track.

## Prerequisites and Installation

Before implementing the pattern, ensure your project includes **GSAP** and the **motion** library for animation and accessibility detection.

```bash
npm install gsap motion

```

## The HorizontalPan Component Architecture

### Client-Side Registration

Create the component at [`src/components/HorizontalPan.tsx`](https://github.com/Leonxlnx/taste-skill/blob/main/src/components/HorizontalPan.tsx). The file must start with the `"use client"` directive since GSAP operates exclusively in the browser. Register the **ScrollTrigger** plugin once at the module level to avoid duplicate registration errors.

### Refs and Accessibility Guards

The component uses two `useRef` hooks: `wrap` for the outer section that gets pinned, and `track` for the inner scrolling container. It also checks `useReducedMotion()` to respect user accessibility preferences, short-circuiting the animation if the user prefers reduced motion.

### Dynamic Distance Calculation

The horizontal travel distance is computed dynamically as `track.current.scrollWidth - window.innerWidth`. This ensures the scroll length perfectly matches the amount of horizontal content overflow, regardless of viewport size.

### ScrollTrigger Configuration

A single `gsap.to` animates the track's `x` property from `0` to `-distance`. The ScrollTrigger configuration pins the wrapper (`pin: true`) when it reaches the top of the viewport (`start: "top top"`), sets the scroll duration to match the travel distance (`end: () => +=${distance}`), and syncs the animation to scroll position with `scrub: 1`.

## Complete Implementation

```tsx
// src/components/HorizontalPan.tsx
"use client";

import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";

gsap.registerPlugin(ScrollTrigger);

export function HorizontalPan({ children }: { children: React.ReactNode }) {
  const wrap = useRef<HTMLDivElement>(null);   // outer section (pinned)
  const track = useRef<HTMLDivElement>(null); // inner flex track
  const reduce = useReducedMotion();

  useEffect(() => {
    // Bail out if reduced-motion is preferred or refs are missing
    if (reduce || !wrap.current || !track.current) return;

    const ctx = gsap.context(() => {
      // Horizontal distance the track must travel
      const distance = track.current!.scrollWidth - window.innerWidth;

      // Animate the track → left as the user scrolls down
      gsap.to(track.current, {
        x: -distance,
        ease: "none",
        scrollTrigger: {
          trigger: wrap.current,
          start: "top top",               // pin when wrapper hits viewport top
          end: () => `+=${distance}`,     // scroll length = travel distance
          pin: true,                      // keep the wrapper fixed while scrolling
          scrub: 1,                       // sync animation to scroll position
          invalidateOnRefresh: true,      // recalc on resize/orientation change
        },
      });
    }, wrap); // limit context to wrapper element

    // Clean up on unmount
    return () => ctx.revert();
  }, [reduce]);

  return (
    <section ref={wrap} className="relative overflow-hidden">
      {/* The flex track that will slide horizontally */}
      <div ref={track} className="flex h-[100dvh] items-center">
        {children}
      </div>
    </section>
  );
}

```

## Usage Example

Import the component and wrap any content you want to scroll horizontally. Each child should occupy the full viewport width to create the panel-sliding effect.

```tsx
import { HorizontalPan } from "@/components/HorizontalPan";

export default function Demo() {
  return (
    <HorizontalPan>
      {/* Any number of full‑width panels – e.g., images or cards */}
      <div className="w-screen flex-none bg-gray-100">Panel 1</div>
      <div className="w-screen flex-none bg-gray-200">Panel 2</div>
      <div className="w-screen flex-none bg-gray-300">Panel 3</div>
    </HorizontalPan>
  );
}

```

## How It Works Under the Hood

According to the taste-skill source code in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) (lines 38-70), the pattern creates a **pinned container** that remains fixed in the viewport while the user scrolls vertically. The **horizontal distance** is calculated by comparing the track's total scroll width against the viewport width.

The animation uses `ease: "none"` to create a direct 1:1 relationship between scroll position and horizontal translation. The **scrub** property set to `1` ensures the animation follows the scroll precisely without smoothing or delay, while `invalidateOnRefresh: true` recalculates distances when the window resizes.

## Summary

- The **HorizontalPan** component in taste-skill converts vertical scroll into horizontal translation using **GSAP ScrollTrigger**.
- The implementation requires `"use client"` and registers ScrollTrigger at the module level in [`src/components/HorizontalPan.tsx`](https://github.com/Leonxlnx/taste-skill/blob/main/src/components/HorizontalPan.tsx).
- **Accessibility** is handled via `useReducedMotion()` from the motion library, which disables the animation when preferred.
- **Dynamic calculation** of `scrollWidth - innerWidth` ensures the scroll distance always matches the content width.
- **Cleanup** is managed through `gsap.context()` and the `ctx.revert()` return function, preventing memory leaks on unmount.

## Frequently Asked Questions

### Why does the component use "use client"?

The `"use client"` directive is required because GSAP and ScrollTrigger access browser-only APIs like `window`, `document`, and `scrollWidth`. Without this directive, Next.js would attempt to render the component on the server, causing reference errors during hydration.

### How does the horizontal-pan pattern handle reduced motion preferences?

The component imports `useReducedMotion` from `motion/react` and checks the returned boolean before initializing any GSAP animations. If the user has enabled reduced motion in their system settings, the effect short-circuits and the content displays as a standard vertical layout without pinning or horizontal translation.

### What happens if the content width changes after initial render?

The ScrollTrigger configuration includes `invalidateOnRefresh: true`, which forces GSAP to recalculate the scroll distance when the window resizes or orientation changes. For dynamic content changes after mount, you would need to manually call `ScrollTrigger.refresh()` or reinitialize the animation within a `useEffect` watching the content dependencies.

### Where is the canonical reference for this implementation?

The canonical implementation is documented in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) at lines 38-70 in the Leonxlnx/taste-skill repository. This file contains the annotated skeleton, implementation notes, and architectural conventions specific to the taste-skill codebase.