# How to Implement the Horizontal-Pan Scroll Pattern in Taste Skill

> Learn to implement the horizontal-pan scroll pattern in Taste Skill. Pin containers with GSAP ScrollTrigger and animate x position for smooth horizontal scrolling, respecting reduced-motion.

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

---

**The horizontal-pan scroll pattern in Taste Skill converts vertical scrolling into a smooth horizontal translation by pinning a container with GSAP ScrollTrigger and animating the track's `x` position based on `track.scrollWidth - window.innerWidth`, while respecting reduced-motion preferences via `motion/react`.**

The horizontal-pan scroll pattern is a core interaction in the Taste Skill design system that transforms native vertical scroll into a cinematic horizontal gallery. As documented in the `taste-skill` repository, this pattern relies on GSAP ScrollTrigger to hijack scroll progress and drive a flex-based track across the viewport. In this guide, you will learn the exact implementation used in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) and how to drop it into any React or Next.js project.

## What Is the Horizontal-Pan Scroll Pattern?

According to the Taste Skill source code, the Horizontal-Pan pattern is a scroll-driven animation that maps vertical wheel and trackpad movement to a negative `x` translation of a content track. It is defined in the GSAP Horizontal-Pan implementation section of [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) (lines 438–473). This effect is ideal for gallery sections, product carousels, and storytelling flows where panels should move horizontally while the page remains in a pinned viewport state.

## Prerequisites and Dependencies

Before implementing the pattern, confirm you have the following dependencies installed. The Taste Skill specification explicitly requires these packages to guarantee performance and accessibility.

### GSAP and ScrollTrigger

The animation engine is **GSAP** with the **ScrollTrigger** plugin. Register the plugin once at the module level with `gsap.registerPlugin(ScrollTrigger)` so the scroll-scrubbing and pinning logic is available throughout the component tree.

### Motion/React for Accessibility

The `motion/react` package provides the **`useReducedMotion`** hook. This is mandatory for any component where `MOTION_INTENSITY` exceeds level 3. If the user prefers reduced motion, the GSAP animation must be skipped entirely.

### Tailwind CSS Utilities

Tailwind CSS supplies the layout primitives used in the canonical skeleton: `relative` and `overflow-hidden` on the wrapper, plus `flex` and `h-[100dvh]` on the track. These classes ensure the section occupies the full dynamic viewport height without extraneous stylesheet files.

## Architectural Overview

The implementation follows four rigid steps defined in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md):

1. **Pin the wrapper.** The outer `section` element is pinned by ScrollTrigger so it remains locked in the viewport during the horizontal traverse.
2. **Measure scroll distance.** Calculate `const distance = track.current!.scrollWidth - window.innerWidth`. This is the total horizontal overflow that must be translated.
3. **Animate the track’s `x` property.** Use `gsap.to(track.current, { x: -distance, ease: "none", ... })` with a `scrollTrigger` configuration that starts at `top top` and ends after the full horizontal distance.
4. **Guard against reduced motion.** Check `useReducedMotion()` before instantiating any GSAP context. If true, return early and let the children render statically.

## Minimal Component Implementation

The canonical skeleton from [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) (lines 438–473) maps directly to the following copy-paste-ready React component. Save this as [`src/components/HorizontalPan.tsx`](https://github.com/Leonxlnx/taste-skill/blob/main/src/components/HorizontalPan.tsx).

```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);
  const track = useRef<HTMLDivElement>(null);
  const reduced = useReducedMotion();

  useEffect(() => {
    if (reduced || !wrap.current || !track.current) return;

    const ctx = gsap.context(() => {
      const distance = track.current!.scrollWidth - window.innerWidth;
      gsap.to(track.current, {
        x: -distance,
        ease: "none",
        scrollTrigger: {
          trigger: wrap.current,
          start: "top top",
          // end = scroll distance = horizontal travel needed
          end: () => `+=${distance}`,
          pin: true,
          scrub: 1,
          invalidateOnRefresh: true,
        },
      });
    }, wrap);

    return () => ctx.revert();
  }, [reduced]);

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

```

> **Key points:** `start: "top top"` pins the section when its top edge hits the top of the viewport, `pin: true` keeps the wrapper stationary, and `scrub: 1` ties the animation directly to the scroll position. The component respects `prefers-reduced-motion` automatically.

## Using the Component in a Page

Import the component into any Next.js 13+ page or React route. Each child inside the track must declare a **fixed width**—such as `w-screen` or an explicit pixel value—so that `scrollWidth` can be measured accurately.

```tsx
// src/app/page.tsx (Next.js 13+)
import { HorizontalPan } from "@/components/HorizontalPan";

export default function HomePage() {
  return (
    <>
      {/* … other sections … */}

      <HorizontalPan>
        {/* Example: three full‑width panels */}
        <div className="w-screen bg-blue-500 flex items-center justify-center">
          <h1 className="text-5xl text-white">Panel 1</h1>
        </div>
        <div className="w-screen bg-green-500 flex items-center justify-center">
          <h1 className="text-5xl text-white">Panel 2</h1>
        </div>
        <div className="w-screen bg-purple-500 flex items-center justify-center">
          <h1 className="text-5xl text-white">Panel 3</h1>
        </div>
      </HorizontalPan>

      {/* … other sections … */}
    </>
  );
}

```

Because the parent `section` is pinned, vertical scroll on the page drives the horizontal pan of the track.

## Handling Reduced-Motion Preferences

For users who prefer reduced motion, conditionally render a static layout. The Taste Skill specification mandates skipping the GSAP animation entirely rather than simply slowing it down.

```tsx
{reduced ? (
  <div className="grid gap-8">{children}</div>
) : (
  <HorizontalPan>{children}</HorizontalPan>
)}

```

When `reduced` is true, the track becomes a standard vertical grid that respects the user’s accessibility settings.

## Key Files in the Taste Skill Repository

The following files in `Leonxlnx/taste-skill` define the authoritative specification for the horizontal-pan scroll pattern:

- **[`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md)** (lines 438–473): Contains the Horizontal-Pan Canonical Skeleton and the exact GSAP + ScrollTrigger parameters.
- **[`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md)** (lines 363–364): Documents the high-level design rationale and common pitfalls for the GSAP Horizontal-Pan Pattern.
- **[`skills/taste-skill/blocks/transition/horizontal-pan.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/blocks/transition/horizontal-pan.md)**: The block-library entry that downstream agents reference when generating code.
- **[`README.md`](https://github.com/Leonxlnx/taste-skill/blob/main/README.md)**: Explains how to install the skill via `npx skills add` so the pattern becomes available to your project.
- **`assets/readme-banner.png`**: Optional visual reference demonstrating the intended visual direction for horizontal-pan sections.

## Summary

- The horizontal-pan scroll pattern converts vertical scroll into horizontal translation by scrubbing a pinned track's `x` position.
- Use `gsap.to()` with `scrollTrigger: { pin: true, scrub: 1, start: "top top", end: () => `+=${distance}` }` as implemented in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md).
- Always wrap the logic in a `useReducedMotion()` guard from `motion/react` to comply with Taste Skill accessibility requirements.
- Children inside the track need fixed widths so `track.scrollWidth - window.innerWidth` returns the correct travel distance.
- Clean up GSAP contexts with `ctx.revert()` inside the `useEffect` return to prevent memory leaks.

## Frequently Asked Questions

### What is the horizontal-pan scroll pattern in Taste Skill?

The horizontal-pan scroll pattern is a scroll-hijack interaction that translates vertical page scrolling into a horizontal pan of a flex-based content track. It is defined in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) and relies on GSAP ScrollTrigger to pin the section and scrub the animation in real time.

### How does the ScrollTrigger end value work in the HorizontalPan component?

The `end` property is a dynamic function that returns `` `+=${distance}` `` where `distance` equals `track.scrollWidth - window.innerWidth`. This tells ScrollTrigger that the scroll-linked animation should span exactly the number of pixels required to move the track fully into view.

### Why must children have a fixed width inside the horizontal track?

If children shrink or lack explicit width, the track’s `scrollWidth` collapses to the viewport width, making `distance` evaluate to zero. Fixed widths such as `w-screen` ensure the browser calculates positive overflow, which is the travel distance the GSAP animation needs.

### How do I respect reduced-motion preferences when using this pattern?

Use the `useReducedMotion` hook from `motion/react` to detect the preference. When `reduced` is true, skip the `gsap.context()` setup and render a static vertical stack instead, as mandated by the Taste Skill reduced-motion guardrails.