How I rebuilt my website with Astro, Swup and a bit of GSAP magic
How I rebuilt my website with Astro, Swup and a bit of GSAP magic
META TITLE: Rebuilding with Astro, Swup & GSAP - Developer Guide
META DESCRIPTION: How I rebuilt my portfolio site using Astro, TailwindCSS, Swup.js, and GSAP. What worked, what didn’t, and why this stack surprised me.
READING TIME: 4 minutes
I rebuilt my portfolio site from scratch recently. The stack I landed on worked better than I expected — not because any single piece was impressive on its own, but because they fit together in a way I hadn’t anticipated.
Astro, TailwindCSS, Swup.js, and GSAP gave me something I didn’t think was possible with static sites: the performance of pre-rendered HTML with the feel of a full SPA. Here’s what I actually learned building it.
Starting with Astro
Astro caught my attention for one reason: it generates static HTML by default. No hydration waterfalls, no JavaScript bundles for content that never changes. Just HTML and CSS.
[image: Astro build output showing static HTML generation]
The build speed is genuinely useful. Using Bun.js as the runtime, the entire site rebuilds in under a second — faster than most dev servers can hot-reload. That sounds minor until you’re iterating on content and changes just appear.
The component model feels natural too. Astro components are mostly HTML with just enough JavaScript to handle dynamic bits. No framework lock-in, no build configuration to fight with. Create an .astro file and you’re writing code.
Content collections and frontmatter
Astro handles content through markdown files with frontmatter:
---
title: "Article Title"
date: 2025-12-02
tags: ["web-development", "astro"]
---
Article content here...
These are processed at build time, not runtime. The content collection system validates frontmatter against TypeScript schemas, so errors surface before deployment. No database, no CMS — just files in version control.
[code example: Astro content collection schema definition and querying articles]
Editing content means editing a markdown file. Deploying means pushing to git. It’s boring in the best way.
Swup.js: page transitions for static sites
Astro produces static HTML, so every navigation is a full page load by default. Fast, but abrupt — no transitions, no state preservation.
Swup.js fixes this. It intercepts navigation, fetches the next page via AJAX, and swaps content in-place. The site behaves like an SPA, but the underlying architecture is still static files.
The useful part is the lifecycle hook system. Every navigation fires events: willReplaceContent, contentReplaced, transitionStart, transitionEnd. You decide what happens at each point.
[code example: Swup lifecycle hooks handling page transitions]
In practice, I wire these hooks to GSAP timelines. A link click fires transitionStart — I fade out the current page. When that completes, Swup swaps the content and fires contentReplaced — I fade in the new page.
swup.hooks.on("transitionStart", () => {
gsap.to(".page-content", {
opacity: 0,
duration: 0.3,
ease: "power2.inOut",
});
});
swup.hooks.on("contentReplaced", () => {
gsap.fromTo(
".page-content",
{ opacity: 0 },
{ opacity: 1, duration: 0.3, ease: "power2.inOut" },
);
});
You control every frame. Navigation works exactly how you write it.
GSAP: where the polish comes from
CSS animations work fine for basic fades. But sequencing multiple elements, timing transitions against async events, or coordinating animations across a page swap — that’s where CSS gets awkward fast.
With CSS you set delays manually and hope the timing holds. GSAP timelines let you define a sequence once; the library handles the rest.
[code example: GSAP timeline animating multiple elements in sequence]
For this site, elements get a data-animate attribute. A ScrollTrigger instance watches for viewport entry, fires the animation, then cleans itself up. Simple pattern, works reliably.
The Swup integration is where GSAP earns its place. Page transitions need to wait for animations to finish before swapping content. GSAP timelines expose .then() — hook that into Swup’s before content:replace and the timing just works:
swup.hooks.before("content:replace", async () => {
const timeline = gsap.timeline();
timeline.to(".page-content", { opacity: 0, duration: 0.3 });
await timeline.then();
// Content swap happens here
});
You’re telling the browser exactly what to do, frame by frame, instead of debugging CSS timing edge cases.
[image: DevTools timeline showing smooth GSAP animation performance]
How the pieces fit
Each tool has a specific job. Astro generates static HTML. Tailwind handles styling without bloat. Swup intercepts navigation and fires hooks. GSAP handles animation timing.
Nothing overlaps. There’s no runtime framework loading before content renders, no CSS-in-JS overhead, no animation jank from CSS transitions racing against page loads.
The result is a site that’s fast structurally, not just optimized at the margins. If you’re building something content-focused and want polished interactions without a client-side framework, this combination is worth trying.
Be awesome.
Keep building magic. ✊
Petar 🥃