Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.
canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact
1@keyframes transformRight {
2 from {
3 transform: scaleX(0);
4 }
5 to {
6 transform: scaleX(1);
7 }
8}
9
10@keyframes transformLeft {
11 from {
12 transform: scaleX(1);
13 }
14 to {
15 transform: scaleX(0);
16 }
17}
18
19@keyframes slideUp {
20 from {
21 opacity: 0;
22 transform: translateX(-100vw);
23 }
24 to {
25 opacity: 1;
26 transform: translateX(0);
27 }
28}
29
30.background {
31 position: fixed;
32 top: 0;
33 left: 0;
34 right: 0;
35 bottom: 0;
36 background-color: white;
37 z-index: -2;
38}
39
40.grid {
41 display: grid;
42 grid-template-columns: 50vw 1fr;
43 grid-template-rows: auto;
44 grid-template-areas: "left right";
45 margin-left: 60px;
46 width: calc(100vw - 60px);
47 transform: translateX(-100vw);
48 animation: slideUp 750ms 500ms forwards;
49}
50
51@media (max-width: 1023.98px) {
52 .grid {
53 margin-left: 0;
54 width: 100%;
55 grid-template-columns: 1fr;
56 grid-template-rows: auto auto;
57 grid-template-areas:
58 "left"
59 "right";
60 margin-top: 60px;
61 }
62}
63
64.gridLeft {
65 grid-area: left;
66 background-color: rgba(0, 0, 0, 0.9);
67 min-height: 100vh;
68 display: flex;
69 align-items: center;
70}
71
72@media (max-width: 1023.98px) {
73 .gridLeft {
74 grid-column: 1;
75 grid-row: 1;
76 min-height: auto;
77 align-items: flex-start;
78 }
79}
80
81.gridRight {
82 grid-area: right;
83 height: 100vh;
84 width: 100%;
85 display: flex;
86 align-items: flex-end;
87 overflow-y: auto;
88 background-color: white;
89}
90
91@media (max-width: 1023.98px) {
92 .gridRight {
93 grid-column: 1;
94 grid-row: 2;
95 height: auto;
96 min-height: 50vh;
97 overflow-y: visible;
98 align-items: flex-start;
99 padding-top: 20px;
100 padding-bottom: 30px;
101 }
102}
103
104.contactWrapper {
105 padding-left: 60px;
106}
107
108@media (max-width: 1023.98px) {
109 .contactWrapper {
110 min-height: auto;
111 padding: 10px 20px 30px;
112 }
113}
114
115.heading {
116 font-size: 3em;
117 margin-top: 10px;
118 margin-bottom: 20px;
119}
120
121.heading::before {
122 content: "";
123 display: block;
124 width: 60px;
125 height: 4px;
126 margin-bottom: 10px;
127 background-image: linear-gradient(
128 to right,
129 var(--color-blue) 0,
130 var(--color-purple) 100%
131 );
132}
133
134@media (max-width: 767.98px) {
135 .heading {
136 margin-top: 0;
137 margin-bottom: 10px;
138 }
139}
140
141.shoutOut {
142 text-transform: uppercase;
143 padding: 5px;
144 margin: 0 0 2px 0;
145 float: left;
146 clear: left;
147 font-size: 1.4em;
148 background-image: linear-gradient(
149 to right,
150 var(--color-blue) 0,
151 var(--color-purple) 100%
152 );
153}
154
155@media (max-width: 767.98px) {
156 .shoutOut {
157 font-size: 1.1em;
158 }
159}
160
161.contactList {
162 padding-top: 30px;
163 clear: both;
164}
165
166.contactKey {
167 text-transform: uppercase;
168 font-size: 0.75em;
169 font-weight: 700;
170 color: rgba(255, 255, 255, 0.4);
171 letter-spacing: 2px;
172 font-family: monospace;
173}
174
175.contactValue {
176 font-size: 1.5em;
177 margin-left: 0;
178 margin-bottom: 30px;
179}
180
181.contactLink {
182 color: white;
183 text-decoration: none;
184 position: relative;
185}
186
187.contactLink::before {
188 content: "";
189 position: absolute;
190 left: 0;
191 right: 0;
192 bottom: 0;
193 height: 2px;
194 background: rgba(255, 255, 255, 1);
195 transform-origin: left;
196 animation: transformLeft 300ms normal forwards;
197}
198
199.contactLink::after {
200 content: "";
201 position: absolute;
202 left: 0;
203 right: 0;
204 bottom: 0;
205 height: 2px;
206 background: rgba(255, 255, 255, 0.2);
207}
208
209.contactLink:hover::before {
210 animation: transformRight 300ms normal forwards;
211}
212
213.chat {
214 display: flex;
215 flex-direction: column;
216 justify-content: flex-end;
217 padding: 30px;
218 width: 100%;
219 min-height: 100%;
220}
221
222@media (max-width: 1023.98px) {
223 .chat {
224 justify-content: flex-start;
225 }
226}
227
228.chatLine {
229 display: flex;
230 margin-bottom: 12px;
231 align-items: flex-start;
232}
233
234.chatLineUser {
235 justify-content: flex-end;
236}
237
238.chatBubble {
239 font-size: 1.1em;
240 padding: 12px 18px;
241 background: rgba(0, 0, 0, 0.06);
242 border-radius: var(--radius-2);
243 color: black;
244 max-width: 75%;
245 line-height: 1.5;
246}
247
248@media (max-width: 767.98px) {
249 .chatBubble {
250 font-size: 1em;
251 }
252}
253
254.chatBubble span {
255 display: block;
256 font-size: 0.65em;
257 text-transform: uppercase;
258 margin-top: 8px;
259 color: var(--color-blue);
260 letter-spacing: 2px;
261 font-weight: 700;
262 font-family: monospace;
263}
264
265.chatAvatar {
266 margin-right: 16px;
267 width: 40px;
268 height: 40px;
269 display: inline-flex;
270 flex-shrink: 0;
271}
272
273@media (max-width: 767.98px) {
274 .chatAvatar {
275 margin-right: 10px;
276 width: 36px;
277 height: 36px;
278 }
279}
280
281.chatBubbleUser {
282 background-image: linear-gradient(
283 to right,
284 var(--color-blue) 0,
285 var(--color-purple) 100%
286 );
287 color: white;
288}
289
290.chatBubbleUser span {
291 display: none;
292}
293
294.chatOptions {
295 display: flex;
296 flex-wrap: wrap;
297 gap: 8px;
298 margin-top: 4px;
299 margin-bottom: 12px;
300 padding-left: 56px;
301}
302
303@media (max-width: 767.98px) {
304 .chatOptions {
305 padding-left: 0;
306 }
307}
308
309.chatOption {
310 padding: 8px 16px;
311 font-size: 0.9em;
312 font-weight: 600;
313 background: none;
314 border: 1px solid rgba(0, 0, 0, 0.2);
315 border-radius: var(--radius-2);
316 color: black;
317 cursor: none;
318 transition: border-color 200ms, background 200ms;
319 font-family: inherit;
320}
321
322.chatOption:hover {
323 border-color: var(--color-blue);
324 background: rgba(14, 63, 244, 0.05);
325}
326
327@keyframes dotPulse {
328 0%, 80%, 100% {
329 opacity: 0.3;
330 }
331 40% {
332 opacity: 1;
333 }
334}
335
336.typingDots {
337 display: inline-flex;
338 gap: 4px;
339 align-items: center;
340}
341
342.typingDots span {
343 display: inline-block;
344 width: 6px;
345 height: 6px;
346 background: rgba(0, 0, 0, 0.4);
347 border-radius: 50%;
348 animation: dotPulse 1.4s infinite;
349 margin-top: 0;
350 font-size: inherit;
351 letter-spacing: normal;
352 text-transform: none;
353 font-weight: normal;
354 font-family: inherit;
355 color: transparent;
356}
357
358.typingDots span:nth-child(2) {
359 animation-delay: 0.2s;
360}
361
362.typingDots span:nth-child(3) {
363 animation-delay: 0.4s;
364}