How to Create Horizontal Scroll Pan Animations with GSAP ScrollTrigger in React
You can create horizontal scroll pan animations with GSAP ScrollTrigger by pinning a wrapper element and translating an inner track horizontally based on the calculated scroll distance between the track width and the viewport width, as implemented in the Leonxlnx/taste-skill repository.
The taste-skill repository provides a reusable client-side React component that unlocks smooth horizontal scroll pan animations with GSAP ScrollTrigger. This pattern keeps the wrapper pinned while the user scrolls vertically, driving the inner content sideways to create a cinematic pan effect. All implementation details are documented in the repository's SKILL.md definition file and rely strictly on GSAP and the ScrollTrigger plugin.
How the HorizontalPan Component Works
The canonical implementation in skills/taste-skill/SKILL.md follows a strict seven-step architecture:
- Client-only execution – The file begins with
"use client"so the code runs exclusively in the browser. - DOM refs – Two
useRefhooks capture the wrapper (wrap) that gets pinned and the inner track (track) that slides horizontally. - Reduced-motion guard –
useReducedMotion()frommotion/reactdisables the animation for users who prefer reduced motion. - GSAP Context –
gsap.context()scopes all animations to the wrapper element, making cleanup automatic on unmount. - Distance calculation – The scroll distance equals the total width of the track minus the viewport width.
- Scroll-linked tween – A
gsap.to()tween translates the track leftward while ScrollTrigger maps vertical scroll progress to horizontal movement. - Automatic cleanup – The returned
ctx.revert()call removes triggers and tweens when the component unmounts.
Calculating the Horizontal Distance
Before creating the tween, the component computes how far the track must travel:
const distance = track.current!.scrollWidth - window.innerWidth
This value represents the exact number of pixels the track needs to shift left to reveal its overflow content.
Configuring the ScrollTrigger Pin
The GSAP tween moves the track while ScrollTrigger handles the pinning and scrubbing according to the repository's canonical skeleton:
gsap.to(track.current, {
x: -distance,
ease: "none",
scrollTrigger: {
trigger: wrap.current,
start: "top top",
end: () => `+=${distance}`,
pin: true,
scrub: 1,
invalidateOnRefresh: true,
},
});
Here, start: "top top" pins the wrapper when it reaches the viewport top. The end callback sets the scroll length equal to the horizontal travel distance, and scrub: 1 keeps the animation synchronized with scroll velocity.
Complete HorizontalPan Component
Below is the full reusable component directly from skills/taste-skill/SKILL.md in the Leonxlnx/taste-skill repo.
// 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 reduce = useReducedMotion();
useEffect(() => {
if (reduce || !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: () => `+=${distance}`,
pin: true,
scrub: 1,
invalidateOnRefresh: true,
},
});
}, wrap);
return () => ctx.revert();
}, [reduce]);
return (
<section ref={wrap} className="relative overflow-hidden">
<div ref={track} className="flex h-[100dvh] items-center gap-8">
{children}
</div>
</section>
);
}
The return () => ctx.revert() line in the useEffect cleanup guarantees that every ScrollTrigger instance and tween is destroyed when the component unmounts or reduce changes.
Using the Component with Content Cards
You can drop any child elements into the HorizontalPan component. Here is a practical page example using a row of cards:
// src/app/page.tsx (or any client component)
import { HorizontalPan } from "@/components/HorizontalPan";
export default function Demo() {
return (
<HorizontalPan>
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="w-[300px] h-[400px] bg-white rounded-xl shadow-lg flex items-center justify-center"
>
Card {i + 1}
</div>
))}
</HorizontalPan>
);
}
When the page is scrolled, the wrapper stays pinned while the inner track slides left, producing the smooth horizontal-pan experience.
Tailwind CSS Configuration
The component relies on standard Tailwind utility classes that match the repository's default Tailwind setup. The wrapper uses relative overflow-hidden, while the track uses flex h-[100dvh] items-center gap-8 to maintain a full-viewport height flex container with evenly spaced children.
<section class="relative overflow-hidden">
<div class="flex h-[100dvh] items-center gap-8">
<!-- child elements -->
</div>
</section>
These utilities align with the Tailwind v4 configuration present in the repo's base tailwind.config.js.
Core Source Files
Understanding where these pieces live helps when extending the pattern:
skills/taste-skill/SKILL.md– Documents the Horizontal-Pan canonical skeleton and the GSAP/ScrollTrigger usage details.README.md– Provides general installation steps and guidance on required dependencies such asgsapand@gsap/scrolltrigger.package.json– Listsgsapandmotionas runtime dependencies, confirming the libraries are available for the component.assets/– Contains example imagery that can be placed inside theHorizontalPantrack for visual testing.
Summary
- The HorizontalPan component is a client-only React component that requires the
"use client"directive for browser-only execution. - It calculates horizontal travel distance as
track.current!.scrollWidth - window.innerWidth. gsap.context()scopes all animations to the wrapper and enables one-shot cleanup withctx.revert().- ScrollTrigger pins the wrapper with
pin: true, starts at"top top", and scrubs the horizontal translation usingscrub: 1. useReducedMotion()ensures the animation respects accessibility preferences by exiting early when reduced motion is requested.
Frequently Asked Questions
What does invalidateOnRefresh: true do in the GSAP ScrollTrigger setup?
It instructs ScrollTrigger to recalculate its measurements when the page refreshes or resizes. In the taste-skill implementation, this keeps the horizontal pan animation precise if the viewport width or track content changes after initial load.
Why is the animation wrapped inside gsap.context()?
gsap.context() scopes every tween and trigger created inside its callback to the provided DOM node. Passing wrap as the scope means that calling ctx.revert() on component unmount automatically destroys every GSAP instance without manual cleanup logic.
How does the component handle reduced motion preferences?
The component imports useReducedMotion from motion/react. If the hook returns true, the useEffect exits early before registering any GSAP tweens, ensuring users who prefer reduced motion do not experience forced scroll-driven animations.
Is this pattern compatible with Next.js App Router?
Yes. The "use client" directive at the top of src/components/HorizontalPan.tsx marks the boundary for client execution, making the component fully compatible with the Next.js App Router while preventing server-side rendering issues with browser-only globals like window and document.
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 →