# How to Implement GSAP Sticky-Stack Patterns with ScrollTrigger for Card Animations

> Learn to implement GSAP sticky-stack patterns with ScrollTrigger for dynamic card animations. Pin elements, drive tweens, and eliminate gaps for a seamless scroll effect. Master this technique now.

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

---

**GSAP sticky-stack patterns with ScrollTrigger create a card-stack-on-scroll effect by pinning each card at the viewport top with `start: "top top"` and driving scale and opacity tweens from the next card's scroll position, while `pinSpacing: false` eliminates gaps between pinned elements.**

The GSAP sticky-stack pattern is the canonical method for building vertical carousels of full-page cards that stack on scroll. Defined in the `Leonxlnx/taste-skill` repository under [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) Section 5.A, this pattern is specifically designed for scroll-stack layouts where precise pinning and smooth handoffs between cards are required. The canonical skeleton and critical implementation rules are detailed at lines 362‑366 and lines 665‑724 of the skill guide.

## Why Use the GSAP Sticky-Stack Pattern?

The sticky-stack pattern is purpose-driven motion applied only when a scroll-stack layout is needed, such as a vertical carousel of full-viewport cards. According to the `Leonxlnx/taste-skill` source code, it solves the common "trigger fires halfway" bug by pinning each card at exactly `top top` of the viewport. This ensures that a card stays fixed until the next card reaches the same pinning point, creating a seamless physical stack metaphor.

- **Precise pinning:** Each card is pinned at `top top` and unpinned when the next card arrives at the same coordinate.
- **Visual tightness:** The stack remains compact because `pinSpacing: false` prevents ScrollTrigger from injecting extra wrapper spacing.
- **Scalable architecture:** The pattern scales to any number of cards while keeping the last card static to avoid unnecessary pinning.

## Core Concepts of GSAP Sticky-Stack Patterns with ScrollTrigger

### Client Component Requirement

Every GSAP sticky-stack implementation must begin with the `"use client"` directive because ScrollTrigger relies on browser-side APIs. This directive must be the first line of the component file before any imports.

### Plugin Registration

`gsap.registerPlugin(ScrollTrigger)` must be called once per component file to activate the pinning and scrubbing features globally. Without this registration, `ScrollTrigger.create` and the `scrollTrigger` configuration object inside tweens will throw runtime errors.

### Scoped Context with `gsap.context`

All GSAP calls should be wrapped inside `gsap.context` to enable automatic cleanup. When the React component unmounts, calling `ctx.revert()` destroys all associated tweens, timelines, and ScrollTrigger instances. This prevents memory leaks and stray listeners that can degrade performance on single-page applications.

### Pinning Each Card

`ScrollTrigger.create` pins the card to the viewport top without leaving residual space. The configuration used in the canonical skeleton is:

```tsx
ScrollTrigger.create({
  trigger: card,
  start: "top top",
  end: "top top",
  pin: true,
  pinSpacing: false,
});

```

The `start: "top top"` value is mandatory according to the `taste-skill` design guide at lines 362‑366. Using values like `"top center"` or `"top 80%"` breaks the stack timing. Setting `pinSpacing: false` keeps the document flow tight so the next card slides directly over the pinned card.

### Scale and Opacity Tween

The previous card shrinks and fades while the next card scrolls into view. This is achieved by attaching a `scrollTrigger` object to a `gsap.to` tween on the current card, but using the *next* card as the trigger:

```tsx
gsap.to(card, {
  scale: 0.92,
  opacity: 0.55,
  ease: "none",
  scrollTrigger: {
    trigger: nextCard,
    start: "top bottom",
    end: "top top",
    scrub: true,
  },
});

```

### Reduced Motion Fallback

`useReducedMotion()` from `motion/react` guards the entire effect. If the user prefers reduced motion, the `useEffect` returns early and skips all ScrollTrigger creation. This ensures WCAG-compliant accessibility.

### Responsive Viewport Height

Each card uses `min-h-[100dvh]` to guarantee full viewport coverage regardless of mobile UI chrome. This dynamic viewport unit prevents layout shifts during scroll and ensures a smooth vertical-only experience.

## Step-by-Step Implementation of GSAP Sticky-Stack Patterns

Follow these steps exactly as laid out in the `taste-skill` SKILL.md canonical skeleton at lines 665‑724:

1. **Create a client component** that accepts an array of cards as `ReactNode[]`.
2. **Register GSAP and ScrollTrigger**, then instantiate a `gsap.context` inside a `useEffect`.
3. **Collect card elements** using `gsap.utils.toArray(".stack-card")`.
4. **Loop over the cards** (excluding the last one) and:
   - Pin each card with `ScrollTrigger.create` using `start: "top top"`, `end: "top top"`, `pin: true`, and `pinSpacing: false`.
   - Attach a scale and opacity tween to the current card that is driven by the next card's scroll position.
5. **Render the JSX** with each card wrapped in a container using the classes `stack-card sticky top-0 min-h-[100dvh] flex items-center justify-center`.

## Complete React Component Example

The canonical TypeScript skeleton from [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) is implemented as follows:

```tsx
// StickyStack.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);

/**
 * Stack of cards that stick to the top of the viewport and shrink/ fade
 * as the next card scrolls into view.
 */
export function StickyStack({
  cards,
}: {
  /** Array of React nodes – each will be rendered as a full‑page card */
  cards: React.ReactNode[];
}) {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const prefersReduced = useReducedMotion();

  useEffect(() => {
    // Respect reduced‑motion users
    if (prefersReduced || !wrapperRef.current) return;

    const ctx = gsap.context(() => {
      // Grab every card element
      const cardEls = gsap.utils.toArray<HTMLElement>(".stack-card");

      cardEls.forEach((card, i) => {
        // The very last card stays static – nothing to pin after it
        if (i === cardEls.length - 1) return;

        // Pin the card to the viewport top
        ScrollTrigger.create({
          trigger: card,
          start: "top top",
          end: "top top",
          pin: true,
          pinSpacing: false,
        });

        // Animate the current card based on the NEXT card's scroll position
        gsap.to(card, {
          scale: 0.92,
          opacity: 0.55,
          ease: "none",
          scrollTrigger: {
            trigger: cardEls[i + 1],
            start: "top bottom",
            end: "top top",
            scrub: true,
          },
        });
      });
    }, wrapperRef); // <- scope all GSAP calls to the wrapper element

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

  return (
    <div ref={wrapperRef} className="relative">
      {cards.map((card, i) => (
        <div
          key={i}
          className="stack-card sticky top-0 min-h-[100dvh] flex items-center justify-center"
        >
          {card}
        </div>
      ))}
    </div>
  );
}

```

## Usage Example

Import the `StickyStack` component into any Next.js or React page and pass an array of card nodes:

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

export default function ExamplePage() {
  const demoCards = [
    <div className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white p-12">
      <h1 className="text-5xl font-bold">Card 1</h1>
      <p>First card content…</p>
    </div>,
    <div className="bg-gray-800 text-white p-12">
      <h1 className="text-5xl font-bold">Card 2</h1>
      <p>Second card content…</p>
    </div>,
    <div className="bg-white text-gray-900 p-12">
      <h1 className="text-5xl font-bold">Card 3</h1>
      <p>Third card content…</p>
    </div>,
  ];

  return <StickyStack cards={demoCards} />;
}

```

## Critical Implementation Details

Several constraints from the `taste-skill` source code must be followed to avoid broken animations:

- **Always use `start: "top top"`** on the pin trigger. The design guide at lines 362‑366 explicitly warns against alternatives like `"top center"` or `"top 80%"` because they cause the pin to activate too late.
- **Pin every card except the last one.** The final card has no successor to trigger its unpin, so leaving it static prevents a dangling ScrollTrigger.
- **Set `pinSpacing: false`** on pinned triggers. Without this, GSAP injects spacer elements that create visual gaps between stacked cards.
- **Clean up with `ctx.revert()`.** The `gsap.context` returned function must be invoked in the `useEffect` cleanup phase to destroy all tweens and ScrollTriggers scoped to the wrapper.

## Summary

- **GSAP sticky-stack patterns with ScrollTrigger** are defined in `Leonxlnx/taste-skill` at [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) Section 5.A (lines 665‑724).
- **Pin each card** with `ScrollTrigger.create` using `start: "top top"`, `end: "top top"`, `pin: true`, and `pinSpacing: false`.
- **Animate the handoff** by tweening the current card's `scale` and `opacity` using the next card as the `scrollTrigger` target.
- **Guard for reduced motion** with `useReducedMotion()` from `motion/react` before initializing any GSAP side effects.
- **Scope all animation code** inside `gsap.context` and call `ctx.revert()` on unmount to prevent memory leaks.
- **Use `min-h-[100dvh]`** on card wrappers to ensure full viewport coverage across all devices.

## Frequently Asked Questions

### Why must the ScrollTrigger start value be exactly `"top top"` in a sticky-stack pattern?

According to the `taste-skill` design guide at lines 362‑366, `start: "top top"` ensures that the card pins immediately when its top edge hits the viewport top. Using `"top center"` or `"top 80%"` delays the pin activation, which breaks the stack timing and causes cards to overlap incorrectly or appear to lag behind the scroll position.

### What does `pinSpacing: false` accomplish in GSAP sticky-stack animations?

`pinSpacing: false` tells ScrollTrigger not to insert extra spacer elements after a pinned card. In the sticky-stack pattern implemented in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md), this setting is essential because it keeps the vertical document flow tight. Without it, GSAP would reserve the full height of each pinned card, creating large empty gaps that destroy the layered stack illusion.

### How should reduced motion preferences be handled in sticky-stack card animations?

The canonical skeleton uses `useReducedMotion()` from `motion/react` at the top of the `useEffect`. If the hook returns `true`, the effect returns early and skips all `ScrollTrigger.create` and `gsap.to` calls. This approach guarantees that users who prefer reduced motion receive a static, accessible layout instead of potentially disorienting scroll-linked animations.

### Why is the last card excluded from pinning in the sticky-stack loop?

The implementation iterates over `gsap.utils.toArray(".stack-card")` and returns early when `i === cardEls.length - 1`. Because there is no subsequent card to trigger the unpin or drive the scale tween, pinning the final card would leave an unnecessary and confusing ScrollTrigger active. The last card naturally scrolls into view and rests at the end of the stack.