Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.
canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact
1@keyframes slideLeft {
2 0% {
3 left: -100%;
4 }
5 40% {
6 left: 0;
7 }
8 60% {
9 left: 0;
10 }
11 100% {
12 left: 100%;
13 }
14}
15
16@keyframes quickFadeIn {
17 0% {
18 color: rgba(0, 0, 0, 0);
19 }
20 49% {
21 color: rgba(0, 0, 0, 0);
22 }
23 50% {
24 color: rgba(0, 0, 0, 1);
25 }
26 100% {
27 color: rgba(0, 0, 0, 1);
28 }
29}
30
31@keyframes fadeIn {
32 0% {
33 color: rgba(0, 0, 0, 0);
34 }
35 100% {
36 color: rgba(0, 0, 0, 1);
37 }
38}
39
40.background {
41 position: absolute;
42 top: 0;
43 left: 0;
44 right: 0;
45 bottom: 0;
46 background-color: white;
47 z-index: -2;
48}
49
50.words {
51 position: absolute;
52 z-index: -1;
53 top: 0;
54 bottom: 0;
55 left: 60px;
56 right: 0;
57 opacity: 0.05;
58 font-size: 5vh;
59 line-height: 11vh;
60 color: black;
61 text-transform: uppercase;
62 height: 100vh;
63 overflow: hidden;
64}
65
66@media (max-width: 767.98px) {
67 .words {
68 top: 50px;
69 font-size: 4vh;
70 line-height: 10vh;
71 left: calc(10px + 5%);
72 }
73}
74
75.word {
76 margin: 0;
77 position: absolute;
78 left: 0;
79}
80
81.wordText {
82 position: relative;
83 white-space: nowrap;
84 overflow: hidden;
85 color: rgba(0, 0, 0, 0);
86 display: inline-block;
87 vertical-align: top;
88}
89
90.wordText::before {
91 content: "";
92 position: absolute;
93 width: 100%;
94 height: 100%;
95 background-color: black;
96 left: -100%;
97 z-index: 1;
98}
99
100.wordAppearActive > .wordText,
101.wordEnterActive > .wordText {
102 animation-name: quickFadeIn;
103 animation-duration: 1000ms;
104 animation-fill-mode: forwards;
105}
106
107.wordAppearActive > .wordText::before,
108.wordEnterActive > .wordText::before {
109 animation-name: slideLeft;
110 animation-duration: 1000ms;
111}
112
113.wordExitActive > .wordText {
114 animation-name: quickFadeIn;
115 animation-duration: 1000ms;
116 animation-direction: reverse;
117}
118
119.wordExitActive > .wordText::before {
120 animation-name: slideLeft;
121 animation-duration: 1000ms;
122 animation-direction: reverse;
123}
124
125.wordAppearDone > .wordText,
126.wordEnterDone > .wordText {
127 color: rgba(0, 0, 0, 1);
128}
129
130.paragraph {
131 font-size: 1.8em;
132 font-weight: 300;
133 position: relative;
134 color: rgba(0, 0, 0, 0);
135 animation-name: fadeIn;
136 animation-delay: 1500ms;
137 animation-duration: 1000ms;
138 animation-fill-mode: forwards;
139}
140
141.paragraph::before {
142 content: "";
143 display: block;
144 width: 75px;
145 height: 5px;
146 margin-bottom: 20px;
147 background-color: var(--color-blue);
148}
149
150@media (max-width: 767.98px) {
151 .paragraph {
152 font-size: 1.2em;
153 }
154}
155
156.strong {
157 font-weight: 700;
158 position: relative;
159 white-space: nowrap;
160 overflow: hidden;
161 animation: quickFadeIn 1250ms normal forwards;
162 color: rgba(0, 0, 0, 0);
163 display: inline-block;
164 vertical-align: top;
165}
166
167.strong::before {
168 content: "";
169 position: absolute;
170 width: 100%;
171 height: 100%;
172 background-color: black;
173 left: -100%;
174 z-index: 1;
175 animation: slideLeft 1250ms normal forwards;
176}
177
178.resume {
179 display: flex;
180 align-items: center;
181 width: fit-content;
182 text-decoration: none;
183 margin-top: 20px;
184 font-weight: 700;
185 color: black;
186 position: relative;
187 overflow: hidden;
188}
189
190.resume::before {
191 content: "";
192 display: block;
193 position: absolute;
194 z-index: -1;
195 top: 100%;
196 width: 100%;
197 height: 100%;
198 background: rgba(14, 64, 244, 0.3);
199 transition: top 250ms;
200}
201
202.resume:hover::before {
203 top: 60%;
204}
205
206.resume svg {
207 margin-right: 10px;
208}