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 lightboxFadeIn {
20 from {
21 opacity: 0;
22 }
23 to {
24 opacity: 1;
25 }
26}
27
28@keyframes lightboxScaleIn {
29 from {
30 opacity: 0;
31 transform: scale(0.92);
32 }
33 to {
34 opacity: 1;
35 transform: scale(1);
36 }
37}
38
39@keyframes lightboxPulse {
40 0%, 100% {
41 opacity: 0.4;
42 }
43 50% {
44 opacity: 1;
45 }
46}
47
48.background {
49 position: absolute;
50 top: 0;
51 left: 0;
52 right: 0;
53 bottom: 0;
54 background-color: white;
55 z-index: -2;
56}
57
58.heading {
59 font-weight: 700;
60 margin-top: 60px;
61 margin-bottom: 10px;
62 font-size: 2.5em;
63 color: black;
64}
65
66.heading::before {
67 content: "";
68 display: block;
69 width: 50px;
70 height: 5px;
71 margin-bottom: 20px;
72 background-image: linear-gradient(
73 to right,
74 var(--color-blue) 0,
75 var(--color-purple) 100%
76 );
77}
78
79.paragraph {
80 font-size: 1.3em;
81 margin-top: 0;
82 margin-bottom: 24px;
83 font-weight: 300;
84 color: rgba(0, 0, 0, 0.7);
85 line-height: 1.6;
86}
87
88.subheading {
89 font-weight: 700;
90 margin-top: 48px;
91 margin-bottom: 12px;
92 font-size: 1.6em;
93 color: black;
94 display: flex;
95 align-items: center;
96 gap: 12px;
97}
98
99.subheading > span {
100 font-family: monospace;
101 font-size: 0.7em;
102 padding: 2px 8px;
103 background-image: linear-gradient(
104 to right,
105 var(--color-blue) 0,
106 var(--color-purple) 100%
107 );
108 color: white;
109 border-radius: var(--radius-2);
110 letter-spacing: 1px;
111}
112
113.paragraph a {
114 color: black;
115 text-decoration: none;
116 position: relative;
117 white-space: nowrap;
118}
119
120.paragraph a::before {
121 content: "";
122 position: absolute;
123 left: 0;
124 right: 0;
125 bottom: 0;
126 height: 2px;
127 background: rgba(0, 0, 0, 1);
128 transform-origin: left;
129 animation: transformLeft 300ms normal forwards;
130}
131
132.paragraph a::after {
133 content: "";
134 position: absolute;
135 left: 0;
136 right: 0;
137 bottom: 0;
138 height: 2px;
139 background: rgba(0, 0, 0, 0.2);
140}
141
142.paragraph a:hover::before {
143 animation: transformRight 300ms normal forwards;
144}
145
146.artGrid {
147 display: grid;
148 grid-template-columns: repeat(2, 1fr);
149 gap: 16px;
150}
151
152@media (max-width: 767.98px) {
153 .artGrid {
154 grid-template-columns: 1fr;
155 }
156}
157
158.artItem {
159 position: relative;
160 overflow: hidden;
161 border-radius: var(--radius-2);
162 transition: transform 300ms, box-shadow 300ms;
163}
164
165.artItem:hover {
166 transform: translateY(-3px);
167 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
168}
169
170.cardImage {
171 display: block;
172 background-color: var(--color-primary);
173 aspect-ratio: 16 / 9;
174 overflow: hidden;
175}
176
177.cardImage img {
178 object-fit: cover;
179 object-position: center;
180 width: 100%;
181 height: auto;
182 aspect-ratio: 16 / 9;
183 display: block;
184 opacity: 0;
185 transition: opacity 400ms ease-out;
186}
187
188.cardImage img.imgLoaded {
189 opacity: 1;
190}
191
192.artItemHeader {
193 margin: 0;
194 padding: 10px 14px;
195 display: flex;
196 justify-content: space-between;
197 align-items: center;
198 font-weight: 700;
199 font-size: 0.95em;
200 background: black;
201 color: white;
202}
203
204.artItemHeader > span {
205 font-family: monospace;
206 font-weight: 400;
207 font-size: 1em;
208 color: rgba(255, 255, 255, 0.8);
209 letter-spacing: 1px;
210}
211
212.artItemButton {
213 padding: 8px 14px;
214 font-size: 0.9em;
215 text-decoration: none;
216 font-weight: 700;
217 text-transform: uppercase;
218 display: inline-block;
219 letter-spacing: 2px;
220 color: white;
221 background-image: linear-gradient(
222 to right,
223 var(--color-blue) 0,
224 var(--color-purple) 100%
225 );
226 border-radius: var(--radius-2);
227 transform: scale(1);
228 transition: transform 250ms;
229 margin-bottom: 20px;
230}
231
232.artItemButton:hover {
233 transform: scale(1.1);
234}
235
236.artContainer {
237 width: 100%;
238 height: 500px;
239 max-height: 100vh;
240 background: black;
241 margin-bottom: 16px;
242 position: relative;
243 overflow: hidden;
244 border-radius: var(--radius-2);
245}
246
247.playButton {
248 position: absolute;
249 top: 16px;
250 right: 16px;
251 width: 44px;
252 height: 44px;
253 border: 1px solid rgba(255, 255, 255, 0.2);
254 border-radius: var(--radius-2);
255 background: rgba(0, 0, 0, 0.5);
256 color: white;
257 font-size: 20px;
258 line-height: 1;
259 padding: 0;
260 cursor: pointer;
261 display: flex;
262 align-items: center;
263 justify-content: center;
264 transition: border-color 200ms, background 200ms;
265 z-index: 5;
266}
267
268.playButton:hover {
269 border-color: var(--color-blue);
270 background: rgba(14, 63, 244, 0.2);
271}
272
273.playButton:active {
274 transform: scale(0.95);
275}
276
277/* Lightbox */
278
279.lightboxOverlay {
280 position: fixed;
281 top: 0;
282 left: 0;
283 right: 0;
284 bottom: 0;
285 background: rgba(0, 0, 0, 0.92);
286 display: flex;
287 align-items: center;
288 justify-content: center;
289 z-index: 99999;
290 animation: lightboxFadeIn 300ms ease-out;
291}
292
293.lightboxImageWrapper {
294 position: relative;
295 width: 90vw;
296 height: 90vh;
297 animation: lightboxScaleIn 400ms cubic-bezier(0.16, 1, 0.3, 1);
298 background: black;
299 border-radius: var(--radius-2);
300 overflow: hidden;
301}
302
303.lightboxImage {
304 visibility: hidden;
305 opacity: 0;
306 transition: opacity 300ms ease-out;
307}
308
309.lightboxLoading {
310 position: absolute;
311 top: 50%;
312 left: 50%;
313 transform: translate(-50%, -50%);
314 color: var(--color-blue);
315 font-family: monospace;
316 font-size: 1.2em;
317 letter-spacing: 4px;
318 text-transform: uppercase;
319 animation: lightboxPulse 1.5s ease-in-out infinite;
320}
321
322.show {
323 opacity: 1;
324 visibility: visible;
325}
326
327.hide {
328 opacity: 0;
329 visibility: hidden;
330}
331
332.lightboxClose {
333 position: absolute;
334 top: 16px;
335 right: 16px;
336 width: 40px;
337 height: 40px;
338 background: rgba(0, 0, 0, 0.6);
339 border: 1px solid rgba(255, 255, 255, 0.2);
340 border-radius: var(--radius-2);
341 color: white;
342 font-size: 1.2em;
343 display: flex;
344 align-items: center;
345 justify-content: center;
346 transition: border-color 200ms, background 200ms;
347 z-index: 1;
348}
349
350.lightboxClose:hover {
351 border-color: var(--color-blue);
352 background: rgba(14, 63, 244, 0.2);
353}