TweenLabs LogoTweenLabs
TweenLabs/Components/Source Code

Border Reveal Code

Endpoint: /03-inward-outward-border-reveal

Premium horizontal text scroll where letters fly in and out from top/bottom screen borders.

šŸ“¦ 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
"use client";

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

gsap.registerPlugin(useGSAP, ScrollTrigger);

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

  useGSAP(
    () => {
      const scroller = containerRef.current?.closest("#main-scroller") || undefined;
      const chars = gsap.utils.toArray<HTMLElement>(".reveal-char");

      // 1. Create the main horizontal translation tween
      const horizontalTween = gsap.to(textTrackRef.current, {
        x: () => {
          const track = textTrackRef.current;
          return track ? -(track.scrollWidth - window.innerWidth) : 0;
        },
        ease: "none",
        scrollTrigger: {
          trigger: scrollSectionRef.current,
          scroller: scroller,
          pin: true,
          scrub: 1,
          start: "top top",
          end: "+=3000", // Fixed scroll height to guarantee user can always scroll fully
          invalidateOnRefresh: true,
        },
      });

      // 2. Animate characters relative to their position in the viewport
      chars.forEach((char) => {
        // Unified entry/exit behavior for all letters: enter from top, exit to bottom
        const startY = -window.innerHeight * 0.9; // Starts above screen
        const startRot = -35; // Standardized tilt

        const tl = gsap.timeline({
          scrollTrigger: {
            trigger: char,
            containerAnimation: horizontalTween, // Link to the horizontal scroll timeline
            scroller: scroller,
            start: "left right", // Starts exactly when left edge of character enters right side of viewport (keyword syntax)
            end: "right left", // Ends exactly when right edge of character leaves left side of viewport (keyword syntax)
            scrub: true,
          },
        });

        tl.fromTo(
          char,
          {
            y: startY,
            rotation: startRot,
            opacity: 0,
            scale: 0.6,
          },
          {
            y: 0,
            rotation: 0,
            opacity: 1,
            scale: 1,
            duration: 0.35, // Slower entry
            ease: "power3.out",
          },
        )
          // Add subtle kinetic wave motion while scrolling through the DOM
          .to(char, {
            y: -15,
            rotation: 4,
            scale: 1.03,
            duration: 0.15,
            ease: "sine.inOut",
          })
          .to(char, {
            y: 15,
            rotation: -4,
            scale: 0.97,
            duration: 0.15,
            ease: "sine.inOut",
          })
          .to(char, {
            // Exit to the bottom
            y: -startY, // Positive y value (exits to bottom)
            rotation: -startRot, // Positive 35 degrees
            opacity: 0,
            scale: 0.6,
            duration: 0.35, // Slower exit
            ease: "power3.in",
          });
      });

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

      // Modern fonts ready API to dynamically refresh triggers when custom fonts swap in
      if (document.fonts) {
        document.fonts.ready.then(() => {
          ScrollTrigger.refresh();
        });
      }

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

      return () => {
        window.removeEventListener("load", handleLoad);
        clearTimeout(timer);
        // Let the useGSAP hook handle ScrollTrigger cleanup automatically to avoid React unmount crashes
      };
    },
    { scope: containerRef },
  );

  // Global index tracker for continuous alternation
  let globalCharIndex = 0;

  const renderWord = (word: string, isOrange = false) => {
    return (
      <span className="inline-block whitespace-nowrap">
        {word.split("").map((char) => {
          const idx = globalCharIndex++;
          return (
            <span
              key={idx}
              className="reveal-char inline-block transform origin-center will-change-transform font-serif font-black uppercase text-[8vw] md:text-[10vw] text-white"
              style={{
                textShadow: "4px 4px 0px #121212",
                color: isOrange ? "var(--color-wtf-orange)" : "white",
              }}
            >
              {char}
            </span>
          );
        })}
      </span>
    );
  };

  return (
    <div
      className="relative min-h-screen bg-[#1e1e1e] text-white overflow-x-hidden selection:bg-wtf-yellow selection:text-black"
      ref={containerRef}
    >
      {/* Subtle 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"
        >
          ← Back
        </button>
      </div>

      {/* Scroll Indicator */}
      <div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40 font-mono text-xs uppercase tracking-widest text-white/50 animate-bounce pointer-events-none flex flex-col items-center gap-1">
        <span>Scroll to Explore</span>
        <span className="text-wtf-orange font-bold text-sm">↓</span>
      </div>

      {/* Main assembly wrapper */}
      <div
        ref={scrollSectionRef}
        className="h-[calc(100vh-64px)] w-full flex items-center relative overflow-hidden"
      >
        {/* Horizontal text scroll track */}
        <div
          ref={textTrackRef}
          className="relative flex items-center pl-[100vw] pr-[100vw] whitespace-nowrap h-full select-none z-20 w-max flex-shrink-0"
        >
          <div className="flex gap-[6vw] flex-shrink-0 w-max">
            {renderWord("HELLO")}
            {renderWord("THIS")}
            {renderWord("IS")}
            {renderWord("GSAP", true)}
          </div>
        </div>
      </div>
    </div>
  );
}
master*0 ā“§0 ⚠
Ln 201, 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 inward-outward-border-reveal

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

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

Customization & Component Properties

šŸ› ļø Customization & Component Properties (Props)

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

  • phrase (string): The message string that scrolls horizontally and drops characters. Defaults to 'HELLO THIS IS GSAP'.