TweenLabs LogoTweenLabs
TweenLabs/Components/Source Code

Horizontal Cards Code

Endpoint: /04-horizontal-cards-showcase

Premium horizontal scroll layout where colorful Neo-Brutalist cards slide, float, enter from the bottom, and exit off the top of the viewport.

📦 GSAP: ^3.15.0
📦 @gsap/react: ^2.1.2
⚙️ ScrollTrigger: ✅ Required
page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"use client";

import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useRef } from "react";

gsap.registerPlugin(useGSAP, ScrollTrigger);

const cardsData = [
  {
    id: "MOTION",
    title: "FLUID TIMELINES",
    borderColor: "#0c9367", // green
    btnBg: "bg-wtf-green text-white hover:bg-[#0a8059]",
    rotateStart: 6,
    leftPos: "left-[7.5%] md:left-[10vw]",
    tag: "[MOTION-01]",
    footerText: "TWEENLABS ENGINE",
  },
  {
    id: "NEO",
    title: "NEO BRUTALISM",
    borderColor: "#c53b3a", // red
    btnBg: "bg-wtf-red text-white hover:bg-[#aa3231]",
    rotateStart: -4,
    leftPos: "left-[7.5%] md:left-[30vw]",
    tag: "[STYLE-02]",
    footerText: "TWEENLABS DESIGN",
  },
  {
    id: "SCROLL",
    title: "SCROLL TRIGGERS",
    borderColor: "#3b82f6", // blue
    btnBg: "bg-wtf-blue text-white hover:bg-[#2563eb]",
    rotateStart: 5,
    leftPos: "left-[7.5%] md:left-[50vw]",
    tag: "[SCROLL-03]",
    footerText: "TWEENLABS TRIGGERS",
  },
  {
    id: "PHYSICS",
    title: "PHYSICS COLLIDERS",
    borderColor: "#f1b333", // yellow
    btnBg: "bg-wtf-yellow text-black hover:bg-[#d99f26]",
    rotateStart: -6,
    leftPos: "left-[7.5%] md:left-[70vw]",
    tag: "[PHYSICS-04]",
    footerText: "TWEENLABS COLLIDER",
  },
];

export default function AnimationFourPage() {
  const containerRef = useRef<HTMLDivElement>(null);
  const scrollSectionRef = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      const scroller = containerRef.current?.closest("#main-scroller") || undefined;

      // Master timeline linked to vertical scroll pinning
      // Pinned section height is 4500px to ensure smooth scroll scrubbing
      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: scrollSectionRef.current,
          scroller: scroller,
          pin: true,
          scrub: 0.6,
          start: "top top",
          end: "+=2200",
          invalidateOnRefresh: true,
        },
      });

      const cards = gsap.utils.toArray<HTMLElement>(".card-item");

      // Timeline Sequence:
      // 1. Staggered Entry: Cards enter from bottom one by one.
      // 2. Wait Phase: All cards stay on screen together, drifting slightly.
      // 3. Staggered Exit: Cards exit off-screen top one by one.
      cards.forEach((card, idx) => {
        const cardData = cardsData[idx];
        const startRot = cardData.rotateStart;

        // Stagger start times: Card 1 at 0s, Card 2 at 0.6s, Card 3 at 1.2s, Card 4 at 1.8s
        const entryStart = idx * 0.6;
        const entryDuration = 1.0;

        // All cards are fully entered by 2.8s. They all exit together at 3.0s as user scrolls.
        const holdStart = entryStart + entryDuration;
        const exitStart = 3.0; // All cards exit simultaneously
        const holdDuration = exitStart - holdStart;
        const exitDuration = 1.0;

        // 1. Entry: from below the viewport (110vh) up to the center (0)
        tl.fromTo(
          card,
          {
            y: "110vh",
            rotation: startRot + 15,
            opacity: 0,
            scale: 0.85,
          },
          {
            y: "0vh",
            rotation: startRot,
            opacity: 1,
            scale: 1,
            duration: entryDuration,
            ease: "power2.out",
          },
          entryStart,
        )
          // 2. Wait/Float Phase: subtle kinetic drift in the center
          .to(
            card,
            {
              y: "-8vh",
              rotation: startRot - 2,
              duration: holdDuration,
              ease: "none",
            },
            holdStart,
          )
          // 3. Exit: up and out off the top of the viewport (-110vh)
          .to(
            card,
            {
              y: "-110vh",
              rotation: startRot - 15,
              opacity: 0,
              scale: 0.85,
              duration: exitDuration,
              ease: "power2.in",
            },
            exitStart,
          );
      });

      // Continuous idle floating/bouncing effect for cards when standing still
      gsap.to(".card-inner", {
        y: "-10px",
        rotation: "1.5",
        duration: 2.2,
        ease: "sine.inOut",
        yoyo: true,
        repeat: -1,
        stagger: {
          each: 0.35,
          from: "random",
        },
      });

      // Recalculate ScrollTrigger parameters once fonts load
      const handleLoad = () => {
        ScrollTrigger.refresh();
      };
      window.addEventListener("load", handleLoad);

      if (document.fonts) {
        document.fonts.ready.then(() => {
          ScrollTrigger.refresh();
        });
      }

      const timer = setTimeout(() => {
        ScrollTrigger.refresh();
      }, 1500);

      return () => {
        window.removeEventListener("load", handleLoad);
        clearTimeout(timer);
      };
    },
    { scope: containerRef },
  );

  return (
    <div
      className="relative min-h-screen bg-[#1e1e1e] text-white overflow-x-hidden selection:bg-wtf-yellow selection:text-black"
      ref={containerRef}
    >
      {/* Dot Grid Background Overlay */}
      <div
        className="absolute inset-0 dot-grid pointer-events-none z-10"
        style={{ opacity: 0.05 }}
      />

      {/* Dashboard Back Link */}
      <div className="fixed left-6 top-6 z-50">
        <button
          onClick={() =>
            window.history.length > 1
              ? window.history.back()
              : (window.location.href = "/")
          }
          className="brutalist-btn bg-wtf-yellow text-black px-4 py-2 text-xs font-mono font-bold uppercase rounded-md cursor-pointer border-2 border-black shadow-[3px_3px_0px_#000]"
        >
          ← Back
        </button>
      </div>

      <div
        ref={scrollSectionRef}
        className="h-[calc(100vh-64px)] w-full flex items-center justify-center relative overflow-hidden"
      >
        {/* Absolute Cards container */}
        <div className="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none z-20">
          <div className="relative w-full h-[340px] md:h-[380px] lg:h-[420px]">
            {cardsData.map((card) => (
              <div
                key={card.id}
                className={`card-item absolute top-0 ${card.leftPos} w-[85%] md:w-[18vw] h-full transform will-change-transform pointer-events-auto`}
              >
                <div
                  className="card-inner w-full h-full border-4 bg-[#fbfaf7] rounded-3xl flex flex-col justify-between p-6 md:p-8 shadow-[8px_8px_0px_rgba(0,0,0,0.95)]"
                  style={{
                    borderColor: card.borderColor,
                  }}
                >
                  {/* Card Top Left logo */}
                  <div className="flex items-center gap-0.5 font-serif font-black text-xs md:text-sm uppercase text-black select-none">
                    <span className="text-[#c53b3a]">D</span>
                    <span className="text-[#f1b333]">E</span>
                    <span className="text-[#0c9367]">V</span>
                  </div>

                  {/* Card Center Content */}
                  <div className="flex-1 flex flex-col items-center justify-center text-center gap-6">
                    <h3 className="font-serif font-black text-xl md:text-2xl lg:text-3xl text-[#121212] leading-tight tracking-tight uppercase whitespace-normal px-2">
                      {card.title}
                    </h3>
                    <button
                      className={`brutalist-btn ${card.btnBg} px-6 py-2.5 rounded-full font-mono text-xs font-bold uppercase border-2 border-black shadow-[3px_3px_0px_#000] cursor-pointer`}
                    >
                      Learn More
                    </button>
                  </div>

                  {/* Card Bottom Right details */}
                  <div className="flex justify-between items-center w-full font-mono text-[9px] text-zinc-400">
                    <span>{card.tag}</span>
                    <span className="font-bold text-zinc-650 tracking-wider">
                      {card.footerText}
                    </span>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}
master*0 ⓧ0 ⚠
Ln 256, Col 1Spaces: 2UTF-8TypeScript JSXPrettier

⚙️ Setup & Integration Guide

How to install, import, and configure this animation in your project

💻

Option A: Install via CLI (Recommended)

You can install this component directly into your project via the TweenLabs CLI. It automatically creates the file and configures dependencies:

npx tweenlabs add horizontal-cards-showcase

Option B: Manual Installation

Follow these steps to integrate the component into your project manually:

1

Install Packages

First, install GSAP and its official React hook helper library (@gsap/react).

npm install gsap @gsap/react
2

Add Required CSS Styles

Copy the styles from the Required CSS tab above, or open the styles.css file that was automatically downloaded with your component. Paste these classes into your global stylesheet (e.g. src/app/globals.css or similar).

3

Create Component File

Create a new file in your React or Next.js project (e.g. src/components/HorizontalCards.tsx) and paste the code from the Standalone React Component tab above. If no standalone tab is available, copy the full page file code and adjust the routing logic for your needs.

⚠️ ScrollTrigger Plugin Notice

This component uses scroll-triggered timing events. Make sure to register the plugin as shown inside the code:

GSAP Registration
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
4

Import & Render

Import and render the component in your page or view layout:

App Page
import HorizontalCards from "@/components/HorizontalCards";

export default function Page() {
  return (
    <main className="min-h-screen p-8 bg-[#f5f5f5] flex items-center justify-center">
      <HorizontalCards />
    </main>
  );
}
💡

Customization & Component Properties

🛠️ Customization & Component Properties (Props)

You can pass the following settings to configure the layout and animation details:

  • items (Array): A list of showcases. Each showcase item has a title, subtitle, imgUrl, bgColor, and link.