The Canonical GSAP Sticky-Stack Skeleton in taste-skill: Implementation Guide
The canonical GSAP sticky-stack skeleton is a prescribed ScrollTrigger pattern that pins cards to the viewport top while driving their exit animations through the next card's scroll position, ensuring seamless stacking without half-triggered bugs.
The taste-skill repository defines strict implementation standards for scroll-driven card stacks using GSAP. This canonical GSAP sticky-stack skeleton guarantees consistent pinning behavior and eliminates common animation race conditions by centralizing all transforms within a single ScrollTrigger context.
What the Sticky-Stack Pattern Achieves
Each card in a vertical list pins to the top of the viewport while the subsequent card scrolls up behind it. When the next card reaches the viewport top, the previous card shrinks and fades, creating a smooth stack-like transition. The pattern ensures that pinning triggers exactly at the viewport edge rather than mid-scroll, preventing the "fires halfway" bug common in ad-hoc implementations.
Critical Implementation Points
The skeleton mandates six non-negotiable technical requirements to ensure reliability and accessibility.
Exact Viewport Pinning with start: "top top"
The trigger must fire precisely when the element reaches the viewport top. This prevents the card from hanging partially off-screen or activating too early.
ScrollTrigger.create({
trigger: card,
start: "top top", // Critical: pins exactly at viewport top
// ...
});
Seamless Spacing with pin: true and pinSpacing: false
These properties keep the element fixed during scroll without injecting extra DOM space that would break the stack's visual continuity.
pin: true,
pinSpacing: false, // Prevents extra space between stacked cards
Static Final Card
All cards except the last must be pinned. The final card remains static to allow the stack to release naturally; otherwise, the pinned container would never unpin, leaving users stranded at the bottom of the scroll container.
Animation Driven by the Next Card
The current card's exit transform (scale and opacity) must be controlled by the next card's ScrollTrigger using scrub: true. This creates the visual effect of the current card receding as the upcoming card arrives.
gsap.to(card, {
scale: 0.92,
opacity: 0.55,
ease: "none",
scrollTrigger: {
trigger: cardEls[i + 1], // Driven by next card
start: "top bottom",
end: "top top",
scrub: true, // Smooth animation tied to scroll position
},
});
Reduced Motion Accessibility
The implementation must respect user preferences through a useReducedMotion guard. When detected, the entire effect disables immediately to satisfy accessibility requirements.
const reduce = useReducedMotion();
useEffect(() => {
if (reduce || !ref.current) return;
// ... GSAP setup
}, [reduce]);
Scoped Context Cleanup
All GSAP work must live inside a gsap.context tied to the container ref. This guarantees proper cleanup via ctx.revert() on unmount, preventing memory leaks and orphaned ScrollTrigger instances.
const ctx = gsap.context(() => {
// All ScrollTrigger and animation logic here
}, ref);
return () => ctx.revert(); // Clean up on unmount
Complete React/Next.js Implementation
Below is the canonical component ready for production use. Per the specification in skills/taste-skill/SKILL.md at lines 665-674, this implements all critical points including the reduce-motion guard and scoped context cleanup.
"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 StickyStack({ cards }: { cards: React.ReactNode[] }) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
useEffect(() => {
if (reduce || !ref.current) return;
// Scoped context for automatic cleanup
const ctx = gsap.context(() => {
const cardEls = gsap.utils.toArray<HTMLElement>(".stack-card");
cardEls.forEach((card, i) => {
if (i === cardEls.length - 1) return; // Last card stays static
// Pin current card exactly at viewport top
ScrollTrigger.create({
trigger: card,
start: "top top",
endTrigger: cardEls[cardEls.length - 1],
end: "top top",
pin: true,
pinSpacing: false,
});
// Animate current card based on 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,
},
});
});
}, ref);
return () => ctx.revert();
}, [reduce]);
return (
<div ref={ref} 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>
);
}
Source File References
The official specification resides in the taste-skill repository across two key files:
skills/taste-skill/SKILL.md(lines 665-674): Contains the full skeleton with inline comments defining the canonical pattern in Section 5.A.CHANGELOG.md(line 69): Documents the §5.A GSAP Sticky-Stack entry, summarizing the pattern for release tracking and version control.
Summary
start: "top top"ensures pinning triggers exactly when cards hit the viewport edge, eliminating half-fired animations.pinSpacing: falsecreates seamless stacking without extra DOM space between cards.- Static final card allows the stack to release naturally at the end of the scroll container.
- Next-card-driven transforms with
scrub: truecreate the shrink-and-fade effect as subsequent cards arrive. - Reduced-motion guards and
gsap.contextensure accessibility compliance and prevent memory leaks.
Frequently Asked Questions
What makes this pattern "canonical" in taste-skill?
The pattern is designated canonical because it is the prescribed standard documented in Section 5.A of skills/taste-skill/SKILL.md. It centralizes all scroll-driven transforms in a single ScrollTrigger context, eliminating inconsistent implementations and guaranteeing that pinning behavior remains uniform across all agents and developers using the repository.
Why must the last card remain unpinned?
The final card acts as the scroll release mechanism. If all cards were pinned, the ScrollTrigger container would never unpin, preventing users from scrolling past the stack to subsequent content. By leaving the last card static, the stack naturally concludes and releases the viewport lock.
How does the reduce-motion guard integrate with the skeleton?
The useReducedMotion hook from motion/react returns a boolean that immediately exits the GSAP setup effect when true. This prevents any ScrollTrigger registration or pinning, ensuring users who prefer reduced motion encounter a standard static scroll rather than animated transformations, satisfying WCAG accessibility guidelines.
Where is the official sticky-stack specification maintained?
The authoritative definition lives in skills/taste-skill/SKILL.md at lines 665-674, with additional context in CHANGELOG.md at line 69 under the "§5.A GSAP Sticky-Stack" entry. These files serve as the single source of truth for implementation details and version updates.
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 →