Scrolling Stacked Cards

Animate a stack of cards on scroll. Perfect for showcasing features or steps in a sequential, engaging way.

Key Component Features

This is a live demonstration. Scroll down to see it in action.

Fully responsive by default.
Built with Tailwind CSS for easy styling.
Animated with GSAP for smooth performance.
Type-safe with TypeScript.
Composable and highly reusable.

Installation

First, install the necessary dependencies for animation and icons.

tsx
1npm install gsap @gsap/react react-icons

Next, copy and paste the following component code into your project.

components/ui/stacked-cards.tsx
1'use client';
2
3import React, { useRef } from 'react';
4import { gsap } from 'gsap';
5import { ScrollTrigger } from 'gsap/ScrollTrigger';
6import { useGSAP } from '@gsap/react';
7
8gsap.registerPlugin(ScrollTrigger);
9
10
11
12interface CardData {
13  id: string | number;
14  content: React.ReactNode;
15}
16
17
18interface ScrollingStackedCardsProps {
19  title: string;
20  subtitle: string;
21  cards: CardData[];
22  BackgroundIcon?: React.ComponentType<{ className?: string }>;
23  className?: string;
24}
25
26
27export const StackedCards = ({
28  title,
29  subtitle,
30  cards,
31  BackgroundIcon,
32  className = '',
33}: ScrollingStackedCardsProps) => {
34  const mainRef = useRef<HTMLDivElement>(null);
35  const cardsRef = useRef<(HTMLDivElement | null)[]>([]);
36  cardsRef.current = [];
37
38  useGSAP(
39    () => {
40      if (cards.length === 0) return;
41
42      const cardElements = cardsRef.current.filter(c => c !== null) as HTMLDivElement[];
43      
44      const scrollDuration = cards.length * 800;
45
46      const timeline = gsap.timeline({
47        scrollTrigger: {
48          trigger: mainRef.current,
49          pin: true,
50          scrub: 1,
51          start: 'top top',
52          end: `+=${scrollDuration}`,
53          markers: false,
54        },
55      });
56
57      timeline.to('.gsap-title', {
58        y: 20,
59        duration: 0.4,
60      });
61
62      cardElements.forEach((card, index) => {
63        if (index === cardElements.length - 1) return;
64
65        const animDuration = 1;
66        const opacityStartProgress = 0.7;
67        const opacityStartTimeOffset = animDuration * opacityStartProgress;
68        const opacityDuration = animDuration * (1 - opacityStartProgress);
69
70        timeline
71          .to(
72            card,
73            {
74              yPercent: -25,
75              scale: 1.5,
76              duration: animDuration,
77              ease: 'power1.inOut',
78            },
79            '<+0.5',
80          )
81          .to(
82            card,
83            {
84              opacity: 0,
85              duration: opacityDuration,
86              ease: 'power1.in',
87            },
88            `<+${opacityStartTimeOffset}`, 
89          );
90      });
91    },
92    { scope: mainRef, dependencies: [cards] },
93  );
94
95  const addToRefs = (el: HTMLDivElement | null) => {
96    if (el && !cardsRef.current.includes(el)) {
97      cardsRef.current.push(el);
98    }
99  };
100
101  return (
102    <div ref={mainRef} className={`relative ${className}`}>
103      <div className="h-screen w-full flex flex-col items-center justify-center overflow-hidden">
104
105        <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
106          {BackgroundIcon && (
107            <BackgroundIcon className="text-[25rem] text-primary/5" />
108          )}
109          <div className="absolute inset-0 bg-radial-gradient from-primary/10 to-transparent to-70% rounded-full"></div>
110        </div>
111
112        <div className="gsap-title relative z-10 flex flex-col items-center text-center mb-16">
113          <h2 className="font-display text-4xl sm:text-7xl font-bold bg-clip-text text-transparent bg-gradient-to-b from-gray-50 to-gray-400">
114            {title}
115          </h2>
116          <p className="mt-4 max-w-md text-lg text-gray-400">{subtitle}</p>
117        </div>
118
119        <div className="relative w-11/12 md:w-4/6 h-96">
120          {cards.map((card, i) => (
121            <div
122              key={card.id}
123              ref={addToRefs}
124              className="absolute top-0 left-0 w-full h-full p-8 flex items-center justify-center
125                         bg-blue-200/10 backdrop-blur-2xl rounded-3xl border border-white/10
126                         shadow-2xl shadow-primary/10"
127              style={{
128                transform: `translateY(${i * 15}px) scale(${1 - i * 0.04})`,
129                zIndex: cards.length - i,
130              }}
131            >
132              <div className="text-3xl md:text-5xl font-bold text-center text-stone-300 leading-snug">
133                {card.content}
134              </div>
135            </div>
136          ))}
137        </div>
138      </div>
139    </div>
140  );
141};
Popular Components