Building an Astro Portfolio


I haven’t been programming in my spare time in quite a while. If you already code for a living you have to be driven to spend your weekends doing the same thing! And after attending DEVWorld a few weeks ago I’ve found my drive again.

The reason for that were 2 amazing talks I followed there - Elias van Cutsem talking about Astro and Phil Nash talking about the new View Transitions API. While the View Transitions API is far from being supported by all browsers, Astro seems to have excellent polyfills for it already. An Astro project it is!

pnpm create Astro

Basic Styling

Astro’s CLI has an excellent blog template, so I’ll use that to start out with. I use mostly styled-components at for styling at work, so I wanted to use Tailwind as my styling solution. The blog template came with .css files, so I spent a little bit of time fixing that first.

Next, it’s time to do a little bit of design. This isn’t really my strong point, so I tried to stick with mostly generic instead of doing something wild. Regardless, there were still a few major questions to answer:

  • What’s the colour pattern?
  • How does the navigation look like?

Since Astro is a JS-light framework, I wanted to use as little JS as possible for the header. I remember being inspired by this pure HTML/CSS Hamburger when I was just starting out in web development, so I used that as a base. I wanted to use a full-screen sidebar for mobile navigation, but this gave me 2 new problems:

  • The pure HTML/CSS solution doesn’t disable scroll on the rest of the page. The easy answer is to make it fixed / 100dvh / 100vw, but that leads into the second issue:
  • The native scrollbar doesn’t play nice with 100vw usage, and it’s impossible to detect whether there’s a scrollbar present in the root from inside the header’s CSS.

You could ofcourse solve the second part with JavaScript, but calculating the width of the scrollbar through JS and adjusting the transition to account for it seemed like a lot of effort for marginal gains. I know there are libraries that solve this problem for you, but then we’re importing even more JavaScript when I wanted to stay away from it!

Instead, I decided to disable the scrollbar entirely and build my own inside the header. That lets me use a few simple click/scroll handlers while still adhering to “as little JS as possible”. It also gave me an excuse to practice with an accessible scrollbar replacement, which I hadn’t done before. For the hamburger, I ended up with data-toggled attributes over a checked input since that seemed better to set programmatically.

All this gave me a clear picture of what I wanted my end result to look like, and how many different elements I’d be contrasting with eachother. This let me move to the next phase.

Colour Design

The first part is easy - I knew wanted 2 generic black/white colours for contrast in dark/light mode, a grey colour for highlights and at least 2 accent colours. I asked ChatGPT to generate me a set of those colours, but I did end up choosing the accent colours myself.

A palette of colours used on this website

Anyone that knows me will understand why I picked a shade of orange - although this specific shade isn’t that special. However, all lighter shades of orange I tried didn’t work well with both dark and light - so I knew my second accent had to be more than just a shade difference. Green / Lime seemed to make sense, so I landed on #BADA55. Kind of self-explanatory, that one.

Transition to the Interesting Part

The first thing I noticed is how incredibly easy Astro makes it to add View Transitions - it really is just a few lines of code. The opinionated default animation is also very solid! I spent a lot of time going back and forth between the default, sliding left/right and a lot more custom animations.

I spent a lot of time looking at the repo from Phil Nash’ talk too - you can check out the functioning version here and you’ll understand why I was so motivated. However, those demo’s were written using the native CSS API, which doesn’t translate 1-to-1 to Astro’s implementation.

Maybe it’s because I’m not a CSS animation expert, but I couldn’t get it to work within a reasonable time, so I went back to the default for now. I might revisit this in the future! For my next steps, it was time to cosplay Hal from Malcolm in the Middle.

After deploying the first version, I also noticed a few bugs, and most of those led me to even more issues while attempting to solve them.

  • Clicking the current page’s anchor tag in the header caused a redundant Page Transition
  • Astro’s env.IS_DEV / env.IS_PROD were not working correctly
  • While solving the first bug, I also noticed that the Astro.url.pathname prop value differed between dev and prod!

The first issue was relatively simple - all I had to do was write some event listeners to disable the current page’s navigation link. I also noticed that eventlisteners don’t persist through page transitions (which makes sense). Luckily, astro has transition-related events to listen to, and in the callback I re-add the original listeners.

<script>
  const addListeners = () => {
    document.querySelectorAll('.active').forEach((el) => {
      el.addEventListener('click', (e) => {
        e.preventDefault();
      });
    });
  };

  addListeners();

  document.addEventListener('astro:after-swap', () => {
    addListeners();
  });
</script>

The only thing that’s unclear to me is whether the old page’s document and the corresponding event listeners get garbage collected appropriately. Luckily, if that’s an issue, I’m still 100s of pages / consequent user navigations away from it appearing, so I’ll leave it for now.

Adding the appropriate styling for the disabled links with tailwind was honestly the hardest part, but in hindsight I did that entirely wrong. I used conditional classes / template literals instead of something like tw-merge. It’s too much of a pain to go back and refactor it now, though.

Closing Thoughts

For the IS_DEV / IS_PROD env issue, there were some easy alternatives in env.MODE. The Astro.url.pathname inconsistency was also easily fixed, with a trailingSlash: 'never', line in the astro.config.mjs. However, this did show me that Astro still has some growing pains.

Additionally, this combined with the lack of “Preview Mode” akin to this feature from NextJS makes me hesitant to recommend Astro for production usage just yet. Especially since my last recommendation with the App Router has not been smooth sailing either 😅

That said, the ease of use in Astro is high, and it leaves room for more design-oriented front-end devs to work alongside the API-oriented full-stack devs that NextJS is forcing us all to be. I can’t wait to see where this framework goes in the future.