TweenLabs LogoTweenLabs
TweenLabs/Components/Source Code

Accordion Code

Endpoint: /15-morphing-accordion

Vertical accordion showcase where selection morphs page background color and staggers content.

📦 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
257
258
259
260
261
262
263
264
"use client";

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

gsap.registerPlugin(useGSAP);

interface AccordionItem {
  id: number;
  title: string;
  subtitle: string;
  summary: string;
  details: string[];
  color: string;
  bgTint: string;
}

export default function MorphingAccordionPage() {
  const containerRef = useRef<HTMLDivElement>(null);
  const cardRef = useRef<HTMLDivElement>(null);
  const contentRefs = useRef<(HTMLDivElement | null)[]>([]);
  const [activeIndex, setActiveIndex] = useState<number | null>(null);

  const accordionItems: AccordionItem[] = [
    {
      id: 1,
      title: "Creative Design",
      subtitle: "UI & WIREFRAMING",
      summary:
        "Translating brand identities into modern, high-fidelity responsive user interfaces.",
      details: [
        "Figma layouts & design token sync",
        "Neo-Brutalist typography pairings",
        "Vibrant, custom color palettes (HSL)",
        "Asymmetric block grid blueprints",
      ],
      color: "#e55b3c", // wtf-orange
      bgTint: "#fdf6f4",
    },
    {
      id: 2,
      title: "Frontend Architecture",
      subtitle: "NEXT.JS 16 & REACT 19",
      summary:
        "Building robust, hydration-safe code bases using standard modern frameworks.",
      details: [
        "TypeScript static type validation",
        "React Server Components (RSC) integration",
        "Tailwind CSS v4 compile optimizations",
        "Lenis smooth scroll integration",
      ],
      color: "#0c9367", // wtf-green
      bgTint: "#f2fbf7",
    },
    {
      id: 3,
      title: "Motion Choreography",
      subtitle: "GSAP TIMELINES & TWEENS",
      summary:
        "Crafting fluid, natural interactions with premium timing and performance curves.",
      details: [
        "useGSAP clean context safe execution",
        "Staggered entrance reveals & bounces",
        "ScrollTrigger pinning & scroll-scrubs",
        "Custom elastic & bezier ease functions",
      ],
      color: "#6758a5", // wtf-purple
      bgTint: "#f7f6fc",
    },
    {
      id: 4,
      title: "Performance Tuning",
      subtitle: "60 FPS OPTIMIZATION",
      summary:
        "Optimizing code execution to prevent layout thrashing and rendering lag.",
      details: [
        "CSS Transform (x/y/scale) animation prioritisation",
        "will-change and force3D compositor layers",
        "gsap.quickTo for mouse movement triggers",
        "Garbage collection & memory leak cleanup",
      ],
      color: "#3b82f6", // wtf-blue
      bgTint: "#f3f8fe",
    },
  ];

  const { contextSafe } = useGSAP({ scope: containerRef });

  const handleToggle = (index: number) => {
    const isExpanding = activeIndex !== index;
    setActiveIndex(isExpanding ? index : null);

    const activeItem = accordionItems[index];

    contextSafe(() => {
      // Morph the page background & main card border color
      gsap.to(containerRef.current, {
        backgroundColor: isExpanding ? activeItem.bgTint : "#f0eadf",
        duration: 0.5,
        ease: "power2.out",
      });

      gsap.to(cardRef.current, {
        borderColor: isExpanding ? activeItem.color : "#2a2a2a",
        boxShadow: isExpanding
          ? `8px 8px 0px ${activeItem.color}`
          : "6px 6px 0px #2a2a2a",
        duration: 0.5,
        ease: "power2.out",
      });

      // Animate all height transitions
      accordionItems.forEach((_, idx) => {
        const el = contentRefs.current[idx];
        if (!el) return;

        const shouldExpand = isExpanding && idx === index;

        // Calculate details lists animations inside the expanded item
        if (shouldExpand) {
          gsap.to(el, {
            height: "auto",
            opacity: 1,
            marginTop: 16,
            duration: 0.45,
            ease: "power3.inOut",
            overwrite: "auto",
          });

          // Stagger list elements inside the expanding content
          const listItems = el.querySelectorAll(".accordion-detail-item");
          gsap.fromTo(
            listItems,
            { y: 15, opacity: 0 },
            {
              y: 0,
              opacity: 1,
              stagger: 0.08,
              duration: 0.4,
              ease: "power2.out",
              delay: 0.1,
              overwrite: "auto",
            },
          );
        } else {
          gsap.to(el, {
            height: 0,
            opacity: 0,
            marginTop: 0,
            duration: 0.35,
            ease: "power3.inOut",
            overwrite: "auto",
          });
        }
      });
    })();
  };

  return (
    <div
      className="relative min-h-screen bg-[#f0eadf] text-[#2a2a2a] flex flex-col items-center justify-between p-8 selection:bg-wtf-yellow selection:text-black overflow-hidden transition-colors duration-500"
      ref={containerRef}
    >
      <div className="absolute inset-0 dot-grid pointer-events-none z-0" />

      {/* Header Info */}
      <header className="z-10 w-full max-w-2xl text-center flex flex-col gap-4 mt-8">
        <div className="inline-flex self-center items-center gap-2 bg-wtf-purple border-2 border-[#2a2a2a] px-4 py-1.5 rounded-full text-[10px] font-mono font-bold text-white uppercase tracking-widest shadow-[3px_3px_0px_#2a2a2a] tilt-right">
          <span>Component 15</span>
        </div>
        <h1 className="text-4xl md:text-5xl font-serif font-black uppercase tracking-tight text-[#2a2a2a] leading-none">
          Morphing Accordion
        </h1>
        <p className="max-w-md mx-auto text-zinc-700 text-sm leading-relaxed font-sans font-medium">
          Click any accordion header below. Watch the page background color
          morph, the card borders transform, and content reveal with staggered
          transitions.
        </p>
      </header>

      {/* Accordion Container Card */}
      <main className="z-10 w-full max-w-2xl my-12">
        <div
          ref={cardRef}
          className="w-full brutalist-card p-6 md:p-8 bg-white flex flex-col gap-4 transition-all duration-500"
        >
          {accordionItems.map((item, index) => {
            const isOpen = activeIndex === index;
            return (
              <div
                key={item.id}
                className={`border-b-3 border-[#2a2a2a] last:border-b-0 pb-4 last:pb-0 pt-4 first:pt-0 flex flex-col`}
              >
                {/* Header Button */}
                <button
                  onClick={() => handleToggle(index)}
                  className="w-full flex justify-between items-center text-left focus:outline-none cursor-pointer group"
                >
                  <div className="flex flex-col gap-1">
                    <span className="font-mono text-[9px] font-bold text-zinc-400">
                      [{item.subtitle}]
                    </span>
                    <h2
                      className="text-xl md:text-2xl font-serif font-black uppercase tracking-tight transition-colors duration-200 group-hover:text-wtf-orange"
                      style={{ color: isOpen ? item.color : "" }}
                    >
                      {item.title}
                    </h2>
                  </div>
                  <span className="w-8 h-8 rounded-full border-2 border-[#2a2a2a] flex items-center justify-center font-mono font-bold text-sm bg-zinc-50 shadow-[2px_2px_0px_#2a2a2a] group-hover:bg-[#2a2a2a] group-hover:text-white transition-colors">
                    {isOpen ? "−" : "+"}
                  </span>
                </button>

                {/* Content Area */}
                <div
                  ref={(el) => {
                    contentRefs.current[index] = el;
                  }}
                  className="h-0 opacity-0 overflow-hidden flex flex-col gap-4"
                >
                  <p className="text-sm font-sans font-semibold text-zinc-650 leading-relaxed max-w-xl">
                    {item.summary}
                  </p>

                  <div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2">
                    {item.details.map((detail, dIdx) => (
                      <div
                        key={dIdx}
                        className="accordion-detail-item flex items-center gap-2.5 font-mono text-xs text-zinc-700 bg-zinc-50 border border-zinc-350 p-2.5 rounded-lg shadow-sm"
                      >
                        <span
                          className="w-2 h-2 rounded-full border border-black"
                          style={{ backgroundColor: item.color }}
                        />
                        <span>{detail}</span>
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </main>

      {/* Footer link */}
      <footer className="z-10 mb-8">
        <button
          onClick={() =>
            window.history.length > 1
              ? window.history.back()
              : (window.location.href = "/")
          }
          className="brutalist-btn bg-wtf-yellow text-[#2a2a2a] font-mono font-bold text-xs py-3 px-6 rounded-lg uppercase tracking-wider cursor-pointer"
        >
          ← Back
        </button>
      </footer>
    </div>
  );
}
master*0 ⓧ0 ⚠
Ln 264, 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 morphing-accordion

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/Accordion.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 Accordion from "@/components/Accordion";

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

Customization & Component Properties

🛠️ Customization & Component Properties (Props)

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

  • items (Array): An array of accordion data lists. Each item has a title, subtitle, summary, details array, color, and bgTint morph colors.