I’ve been working within the internet trade for seven years, but I by no means took the time to construct a very stable portfolio.
Nicely… that’s not completely correct. I truly spent a whole lot of time beginning portfolio variations, however I by no means completed any of them. I stored iterating, chasing a consequence that felt “adequate,” and each try ultimately became one other restart.
I noticed one thing alongside the best way: you could be your individual hardest consumer.
To make it work this time, I made a decision to deal with my portfolio like an actual venture, with an actual constraint: a deadline. My purpose is to launch in January 2026, which provides me three months to design and develop a completed model and, most significantly, to ship it.
Design and inspirations
On this article, I’ll largely deal with the technical facet of issues and the way these interactions are constructed, somewhat than the design course of itself however I’ll nonetheless share a number of notes on the design decisions alongside the best way.
I designed my portfolio to really feel restrained and exact, mixing minimalist readability with brutalist edge clear, quiet layouts with moments that really feel uncooked and direct. I’m obsessive about finesse: easy interactions, refined transitions, and that further consideration to micro-details that makes every little thing really feel intentional with out being loud.
The work of Brijan Powell, Thomas Monavon, Greg Lallé, and Gil Huybrecht has been an enormous affect right here.
Tech stack
I made a decision to make use of Astro: a framework that’s turn out to be fairly common just lately, and one I genuinely take pleasure in as a result of it makes SSG easy whereas nonetheless letting me hold issues easy with vanilla JavaScript (no React or different JS frameworks).
For animations and interactivity, I naturally went with GSAP, an trade customary for internet animation. I used Lenis for easy scrolling (it pairs very well with GSAP), Three.js for WebGL, and Swup to deal with web page transitions. For styling and structure, I used Tailwind.
I received’t go too deep on the backend, however I used Prismic as a CMS and hosted every little thing on Netlify: a stable different to Vercel, which I’m selecting to step away from (for causes you’ll be able to most likely guess).
Animations & Interactions
Excessive-quality, refined animations that permit me showcase what I can do whereas conserving every little thing minimal and concise. The purpose is to maintain every little thing as easy as attainable.
I’ve been utilizing GSAP for years, and it’s nonetheless my go-to instrument for inventive front-end work. With its plugin ecosystem, we are able to construct all types of animations and interactions whereas conserving issues performant and easy. That’s why this text focuses on GSAP, and I received’t cowl the 3D “rock” animation made with Three.js. That half may simply be a tutorial by itself.
Listed below are the animation varieties we’ll be taking a look at:
- Reveal animations
- Web page transitions
- The vertical slider
- The slider to grid swap
- The preloader
Reveal animations
This half is essential as a result of it defines the location’s rhythm and the way easy every little thing feels.
For textual content, I break up issues into two classes: paragraphs (normally a number of traces) and titles (only one or a number of phrases). Paragraphs will reveal line by line, whereas titles will animate character by character.
To try this, we’ll use GSAP’s SplitText plugin, then animate every line with a easy masks, a refined stagger, and a constant ease. That is most likely probably the most essential components of movement design if you would like every little thing to really feel easy.
When animating traces, I’m utilizing autoSplit: true so the textual content can re-split responsively if the structure modifications. In that case, the animation have to be created contained in the onSplit() callback and returned so GSAP can correctly revert and rebuild it on resize.
// Titles
this.break up = new SplitText(this.ingredient, {
kind: "phrases, chars",
autoSplit: true,
masks: "chars",
charsClass: "char",
onSplit: (self) => {
return gsap.from(self.chars, {
period: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.01,
ease: "expo.out"
});
}
});
// Paragraphs
this.break up = new SplitText(this.ingredient, {
kind: "traces, phrases",
autoSplit: true,
masks: "traces",
linesClass: "line",
onSplit: (self) => {
return gsap.from(self.traces, {
period: 0.9,
yPercent: 105,
stagger: 0.04,
ease: "expo.out"
});
}
});
Relating to pictures and movies, I went for a refined fade-up reveal, with a small stagger to create a delicate delay between visuals within the galleries.
// Galleries
gsap.fromTo(
this.pictures,
{ yPercent: 100, autoAlpha: 0 },
{
yPercent: 0,
autoAlpha: 1,
period: 0.8,
ease: "power3.out",
stagger: 0.1
}
)
Web page transitions
Web page transitions are additionally important in inventive growth. They allow us to keep in charge of the location’s rhythm and hold the expertise feeling steady. As a substitute of a tough lower between two screens, we are able to information the content material swap with refined movement, making navigation smoother and avoiding that “new web page” jolt. There are a number of front-end libraries that assist with this, and for this venture I selected Swup as a result of it’s simple to combine and provides you loads of freedom to orchestrate the animations.
On web page go away, I’m conserving it easy by reversing the reveal animations. This clears the stage so the brand new web page can animate on enter.
I’m an enormous fan of transitions that carry a component from the outgoing web page into the subsequent one. It provides an actual sense of continuity to navigation, and that sort of element typically makes the distinction in how polished every little thing feels. That’s precisely what I needed for the transition to the About web page, the place the “About” menu merchandise turns into the web page title and naturally we reverse it when leaving the web page.
To realize this, we’ll use the Flip plugin. It’s extremely highly effective and, for my part, nonetheless a bit underused. We first seize the hyperlink’s state (place, dimension, typography, and so on.) with Flip.getState(). Then we transfer that very same hyperlink into the title’s spot and match its dimensions and typographic properties. Lastly, Flip.from(state) animates it from the captured state to the brand new structure, scaling it up till it turns into the web page title.
// 1) Seize the hyperlink’s present state (small, within the header)
const state = Flip.getState(hyperlink)
// 2) Conceal the title and match the hyperlink into the title’s place/dimension
gsap.set(title, { opacity: 0 })
Flip.match(hyperlink, title, {
absolute: true,
scale: false,
props: "fontSize,lineHeight,letterSpacing"
})
// 3) Animate from the captured state to the present place (hyperlink “grows” into the title)
Flip.from(state, {
absolute: false,
easy: true,
period: 0.9,
ease: "expo.inOut",
onComplete: () => {
gsap.set(title, { opacity: 1 });
gsap.set(hyperlink, { opacity: 0 })
}
})
I’m utilizing the identical method to animate the transition from the work listing to every work’s element web page.
The vertical slider
For the instances itemizing, the thought was to go for a really brutalist-style interplay: a vertical slider that permits you to scroll by the works, whereas the lively one scales as much as take the highlight. I received’t go into each element right here as a result of, even when it seems to be fairly easy, the animation is definitely fairly complicated and ended up being a good chunk of code. An enormous a part of that complexity comes from efficiency decisions: as a substitute of animating width and peak, I’m scaling the photographs (which is far smoother), however that additionally means it’s important to rigorously recalculate positioning as you scroll.
To construct this type of scroll-controlled interplay, I’m utilizing ScrollTrigger to drive a GSAP timeline.
const timeline = gsap.timeline({
scrollTrigger: {
set off: this.dom,
begin: "top-=6.5% middle",
finish: "backside center-=0.5%",
scrub: true
}
})
timeline.to(picture, {
scaleX: scaleExpanded,
scaleY: scaleExpanded,
force3D: true,
period: 0.8,
ease: "none",
}, index)
timeline.to(picture, {
scaleX: 1,
scaleY: 1,
force3D: true,
period: 1.5,
ease: "none",
delay: 0.3,
}, ">")
For the work element pages, I’m utilizing the identical method to animate the thumbnail navigation, whereas additionally displaying the lively picture at a bigger scale on the left: the largest picture is at all times the presently lively one.
From slider to grid
I actually needed so as to add a bit further that doesn’t essentially convey a lot when it comes to content material (possibly nothing in any respect), however clearly ranges up the movement and is one thing you typically see in portfolios: a structure swap, transferring the works from a vertical slider to a grid.
You most likely guessed it from the transition we constructed earlier: Flip is the proper instrument for this. I merely toggle a CSS class on the wrapper, which fully reshapes the structure.
.--grid-mode {
.projects__wrapper {
@apply col-span-full grid grid-cols-6 gap-12;
}
.projects__item {
&:first-child {
@apply col-span-2;
}
&:nth-child(4) {
@apply col-start-4;
}
&:nth-child(6) {
@apply col-start-1;
}
&:nth-child(8) {
@apply col-start-5;
}
}
}
The concept is to seize the state of my pictures earlier than toggling the CSS class. As soon as the category is utilized, I can merely use Flip.from(state) to animate every ingredient from its captured state to its new dimension and place.
// 1) Seize present state (listing structure)
const state = Flip.getState(targets)
// 2) Toggle structure: CSS does the remaining (grid vs listing)
projectsDOM.classList.add("--grid-mode")
// 3) Animate from captured state to new structure
Flip.from(state, {
absolute: true,
period: 1,
ease: "expo.inOut",
nested: true,
scale: true,
easy: true
})
The preloader
Past the purely inventive facet, the preloader is generally right here to preload media and key belongings, so the location feels sooner and extra dependable. That’s particularly essential in my case as a result of the homepage opens straight on a video that weighs a number of megabytes, so its loading must be anticipated.
A easy counter on prime of a background that ultimately transforms into the video.
The counter animation itself is easy. It’s only a quantity going from 0 to 100 in fourteen steps and with GSAP you will get that “chunky” development through the use of a steps(14) ease.
gsap.to(progressVal, {
period: 3,
ease: 'steps(14)',
worth: 100,
})
After that, I take advantage of Flip (once more) to maneuver the background picture (which is the primary body of the video) to the precise dimension and place of the showreel video on the web page.
const showreelState = Flip.getState(showreel)
showreel.classList.take away("--preloading-showreel")
Flip.from(showreelState, {
absolute: true,
period: 1,
ease: "expo.inOut",
scale: true,
easy: true
})
In parallel, I additionally animate the body that comprises the counter utilizing clip-path.
gsap.fromTo(
background,
{ clipPath: "inset(2.5rem 2.5rem 2.5rem 2.5rem)" },
{
clipPath: "inset(100% 0rem 0rem 0rem)",
period: 1,
ease: "expo.inOut"
}
)
Conclusion
I hope this text was clear and that you just picked up a number of helpful takeaways alongside the best way.
Ultimately, I’m actually pleased with the way it turned out, and I really feel assured in regards to the technical decisions I made. Astro has been a real revelation for me, and GSAP continues to be a library you’ll be able to at all times depend on: it’s extremely versatile, but stays performant and simple to work with. And it exhibits in observe, even with a stable quantity of GSAP animation and a few Three.js on prime, the location nonetheless scores effectively on PageSpeed Insights.

Thanks for studying, and see you round 👋









