How to Implement the Horizontal-Pan Scroll Pattern in Taste Skill
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 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 (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:
- Pin the wrapper. The outer
sectionelement is pinned by ScrollTrigger so it remains locked in the viewport during the horizontal traverse. - Measure scroll distance. Calculate
const distance = track.current!.scrollWidth - window.innerWidth. This is the total horizontal overflow that must be translated. - Animate the track’s
xproperty. Usegsap.to(track.current, { x: -distance, ease: "none", ... })with ascrollTriggerconfiguration that starts attop topand ends after the full horizontal distance. - 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 (lines 438–473) maps directly to the following copy-paste-ready React component. Save this as src/components/HorizontalPan.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: truekeeps the wrapper stationary, andscrub: 1ties the animation directly to the scroll position. The component respectsprefers-reduced-motionautomatically.
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.
// 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.
{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(lines 438–473): Contains the Horizontal-Pan Canonical Skeleton and the exact GSAP + ScrollTrigger parameters.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: The block-library entry that downstream agents reference when generating code.README.md: Explains how to install the skill vianpx skills addso 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
xposition. - Use
gsap.to()withscrollTrigger: { pin: true, scrub: 1, start: "top top", end: () =>+=${distance}}as implemented inskills/taste-skill/SKILL.md. - Always wrap the logic in a
useReducedMotion()guard frommotion/reactto comply with Taste Skill accessibility requirements. - Children inside the track need fixed widths so
track.scrollWidth - window.innerWidthreturns the correct travel distance. - Clean up GSAP contexts with
ctx.revert()inside theuseEffectreturn 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →