Code
app/page.tsx
1"use client";
2
3import { useLayoutEffect, useState } from "react";
4import { gsap } from "gsap";
5import Loader from "./components/loader";
6import Hero from "./components/hero";
7
8const Home = () => {
9 const [loaderFinished, setLoaderFinished] = useState(false);
10 const [timeline, setTimeline] = useState<TimelineMax | null>(null);
11
12 useLayoutEffect(() => {
13 const context = gsap.context(() => {
14 const tl = gsap.timeline({
15 onComplete: () => setLoaderFinished(true),
16 });
17 setTimeline(tl);
18 });
19
20 return () => context.revert();
21 }, []);
22
23 return (
24 <main>{loaderFinished ? <Hero /> : <Loader timeline={timeline} />}</main>
25 );
26};
27
28export default Home;
Installation
Initialized a project and installed the necessary dependencies to use this component.
1. Initialize a new Next.js project:
npx create-next-app@latest my-app
2. Install GSAP
npm install gsap
Add Loader Component
src/components/loader.tsx
1import React, { useEffect, useRef } from "react";
2import { TimelineMax } from "gsap";
3import { words } from "../constants";
4import { collapseWords, introAnimation, progressAnimation } from "../anim";
5
6interface LoaderProps {
7 timeline: TimelineMax | null;
8}
9
10const Loader: React.FC<LoaderProps> = ({ timeline }) => {
11 const loaderRef = useRef(null);
12 const progressRef = useRef(null);
13 const progressNumberRef = useRef(null);
14 const wordGroupsRef = useRef(null);
15
16 useEffect(() => {
17 if (timeline) {
18 const duration = 2; // Set the duration according to your needs
19
20 timeline
21 .add(introAnimation(wordGroupsRef))
22 .add(progressAnimation(progressRef, progressNumberRef), 0)
23 .add(collapseWords(loaderRef), `-=${duration}`);
24 }
25 }, [timeline]);
26
27 return (
28 <div className="loader__wrapper">
29 <div className="loader__progressWrapper">
30 <div className="loader__progress" ref={progressRef}></div>
31 <span className="loader__progressNumber" ref={progressNumberRef}>
32 0
33 </span>
34 </div>
35 <div className="loader" ref={loaderRef}>
36 <div className="loader__words">
37 <div className="loader__overlay"></div>
38 <div ref={wordGroupsRef} className="loader__wordsGroup">
39 {words.map((word, index) => (
40 <span key={index} className="loader__word">
41 {word}
42 </span>
43 ))}
44 </div>
45 </div>
46 </div>
47 </div>
48 );
49};
50
51export default Loader;
Add Hero Component
src/components/hero.tsx
1const Hero = () => {
2 return (
3 <div className="p-4">
4 </div>
5 );
6};
7
8export default Hero;
Add Animations
src/anim/anim.js
1import gsap from "gsap";
2
3export const introAnimation = (wordGroupsRef) => {
4 const tl = gsap.timeline();
5 tl.to(wordGroupsRef.current, {
6 yPercent: -80,
7 duration: 5,
8 ease: "power3.inOut",
9 });
10
11 return tl;
12};
13
14export const collapseWords = (wordGroupsRef) => {
15 const tl = gsap.timeline();
16 tl.to(wordGroupsRef.current, {
17 "clip-path": "polygon(0% 50%, 100% 50%, 100% 50%, 0% 50%)",
18 duration: 3,
19 ease: "expo.inOut",
20 });
21
22 return tl;
23};
24
25export const progressAnimation = (progressRef, progressNumberRef) => {
26 const tl = gsap.timeline();
27
28 tl.to(progressRef.current, {
29 scaleX: 1,
30 duration: 5,
31 ease: "power3.inOut",
32 })
33 .to(
34 progressNumberRef.current,
35 {
36 x: "100vw",
37 duration: 5,
38 ease: "power3.inOut",
39 },
40 "<"
41 )
42 .to(
43 progressNumberRef.current,
44 {
45 textContent: "100",
46 duration: 5,
47 roundProps: "textContent",
48 },
49 "<"
50 )
51 .to(progressNumberRef.current, {
52 y: 24,
53 autoAlpha: 0,
54 });
55
56 return tl;
57};
58
59export const animateTitle = () => {
60 const tl = gsap.timeline({
61 defaults: {
62 ease: "expo.out",
63 duration: 2,
64 },
65 });
66
67 tl.to("[data-hero-line]", {
68 scaleX: 1,
69 })
70 .fromTo(
71 "[data-title-first]",
72 {
73 x: 100,
74 autoAlpha: 0,
75 },
76 {
77 x: 0,
78 autoAlpha: 1,
79 },
80 "<15%"
81 )
82 .fromTo(
83 "[data-title-last]",
84 {
85 x: -100,
86 autoAlpha: 0,
87 },
88 {
89 x: 0,
90 autoAlpha: 1,
91 },
92 "<"
93 );
94
95 return tl;
96};
97
98export const animateImage = () => {
99 const tl = gsap.timeline({
100 defaults: {
101 ease: "expo.out",
102 duration: 1.5,
103 },
104 });
105
106 tl.to("[data-image-overlay]", {
107 scaleY: 1,
108 })
109 .from(
110 "[data-image]",
111 {
112 yPercent: 100,
113 },
114 "<"
115 )
116 .to("[data-image-overlay]", {
117 scaleY: 0,
118 transformOrigin: "top center",
119 })
120 .from(
121 "[data-image]",
122 {
123 duration: 2,
124 scale: 1.3,
125 },
126 "<"
127 );
128
129 return tl;
130};
131
132export const revealMenu = () => {
133 const tl = gsap.timeline();
134
135 tl.fromTo(
136 "[data-menu-item]",
137 {
138 autoAlpha: 0,
139 y: 32,
140 },
141 {
142 autoAlpha: 1,
143 y: 0,
144 stagger: 0.2,
145 ease: "expo.out",
146 duration: 2,
147 }
148 );
149
150 return tl;
151};
Add Styles
src/globals.css
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4
5*,
6*:before,
7*:after {
8 margin: 0;
9 padding: 0;
10 box-sizing: border-box;
11}
12
13html {
14 font-size: calc(100vw / 1728 * 10);
15}
16
17body {
18 font-size: clamp(16px, 1.6rem, 1.6rem);
19 font-family: "Matter Regular", sans-serif;
20
21 font-style: normal;
22 font-variation-settings: "ital" 0, "wght" 400;
23 background-color: white;
24 color: black;
25 min-height: 100vh;
26 text-rendering: optimizeLegibility;
27 -webkit-font-smoothing: antialiased;
28 -moz-osx-font-smoothing: grayscale;
29 width: 100%;
30 letter-spacing: -0.03em;
31}
32
33[data-hidden] {
34 opacity: 0;
35}
36
37h1 {
38 font-size: 16rem;
39 font-weight: 400;
40 letter-spacing: -0.03em;
41}
42
43img {
44 display: block;
45 width: 100%;
46 max-width: 100%;
47}
48
49.loader {
50 clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
51 height: 100%;
52 width: 100%;
53 display: flex;
54 justify-content: center;
55 align-items: center;
56 flex-direction: column;
57 background-color: white;
58 overflow: hidden;
59 z-index: 2;
60}
61
62.loader__wrapper {
63 position: relative;
64 height: 100%;
65 width: 100%;
66 position: fixed;
67 inset: 0;
68 overflow: hidden;
69}
70
71.loader__words {
72 position: relative;
73 overflow: hidden;
74 height: 41.8rem;
75}
76
77.loader__overlay {
78 position: absolute;
79 inset: 0;
80 height: 100%;
81 z-index: 2;
82 background: linear-gradient(
83 to bottom,
84 rgba(255, 255, 255, 0.9),
85 rgba(255, 255, 255, 0.9) 47%,
86 transparent,
87 transparent 47%,
88 transparent,
89 transparent 55%,
90 rgba(255, 255, 255, 0.9) 50%,
91 rgba(255, 255, 255, 0.9)
92 );
93}
94
95.loader__word {
96 display: block;
97 font-size: 3.2rem;
98}
99
100.loader__progressWrapper {
101 position: absolute;
102 bottom: 0;
103 left: 0;
104 height: 5vh;
105 width: 100%;
106 z-index: 3;
107}
108
109.loader__progress {
110 height: 100%;
111 width: 100%;
112 background-color: black;
113 transform: scaleX(0);
114 transform-origin: left center;
115}
116
117.loader__progressNumber {
118 position: absolute;
119 left: -5vw;
120 top: 50%;
121 transform: translateY(-50%);
122 z-index: 4;
123 white-space: nowrap;
124 color: white;
125 font-size: 3.2rem;
126}
127
128.logo {
129 width: 3.8rem;
130 height: 1.9rem;
131}
132
133.hero {
134 position: relative;
135 height: 100vh;
136 overflow: hidden;
137 display: flex;
138 flex-direction: column;
139 justify-content: space-between;
140}
141
142.hero__top {
143 display: flex;
144 justify-content: space-between;
145 align-items: center;
146 margin-inline: 4rem;
147 margin-top: 3.2rem;
148}
149
150.hero__title {
151 position: absolute;
152 top: 50%;
153 left: 4rem;
154 width: calc(100% - 8rem);
155 transform: translateY(-50%);
156 margin-bottom: 8rem;
157 display: grid;
158 grid-template-columns: max-content 1fr max-content;
159 align-items: center;
160 gap: 3.2rem;
161 font-size: 16rem;
162}
163
164.hero__line {
165 display: inline-block;
166 height: 0.4rem;
167 width: 100%;
168 background-color: black;
169 transform: scaleX(0);
170 transform-origin: center center;
171}
172
173.hero__image {
174 overflow: hidden;
175 position: absolute;
176 bottom: -10vh;
177 left: 0;
178 width: 100%;
179 transform-origin: top center;
180}
181
182.hero__imageOverlay {
183 position: absolute;
184 inset: 0;
185 z-index: 3;
186 background-color: black;
187 transform: scaleY(0.31);
188 transform-origin: bottom center;
189}