All projects

Vue · Frontend

Gallery

An Unsplash browser built to learn Vue 3, aimed at one problem: an image grid that does not jump.

Live 2024 Creator
masonry-unsplash.netlify.app
The Gallery app showing a search field above a grid of Unsplash photographs at varying heights.
Cards vary in height from CSS grid row spans. No layout library, and no JavaScript measuring anything.

Overview

What it is

Gallery reads the Unsplash API and presents it as a stacked grid with keyword search, infinite scroll, and a lightbox. It is built on Vue 3 with the Composition API and TypeScript.

The whole app is arranged around one goal: the layout must not move while content arrives. Everything else follows from that.

Motivation

Why I built it

I wanted to learn the Vue 3 Composition API properly rather than read about it, so I needed a small project with a real problem in it.

I picked an image grid because layout shift is the one genuinely interesting thing in an otherwise simple app. A grid that reflows every time a photo decodes is the difference between an app feeling fast and feeling broken, and getting that right is more subtle than it looks.

It is a small app built to learn a framework, and the layout-shift work in it is the part worth reading.

Decisions

How it works

01

Skeletons are real list items

The list holds either a photo or a skeleton marker. Placeholders take the exact grid slots their photos will take, so an arriving image replaces one in place and nothing below it moves. This is the whole trick, and it is why the grid holds still.

02

Masonry from CSS grid, nothing else

Fine auto-rows plus alternating row spans give the staggered look. No layout library and no JavaScript measuring element heights, which also means no second reflow after measurement.

03

Pagination guarded on three conditions

A throttled scroll watcher fires the next page at the bottom, but only when no request is open, no error is showing, and the last page has not been reached. Infinite scroll without those guards hammers the API past the end of the results.

Friction

What was hard

Failing without stranding the user

When a page request fails, the list truncates back to the last complete page so the skeletons do not sit there forever, and a retry re-requests exactly that page. Getting the rollback right took longer than the happy path.

One list serving two modes

Browse and search hit different endpoints, return different shapes, and disagree about whether a total page count exists. Normalising on the way in keeps the view identical for both, but the pagination guard has to treat an unknown total as open-ended without looping.

The state of it

Where it stands

The limitations, in plain terms. Every project here is something I built and use, not something I am selling.

Stack

Vue 3 TypeScript Vite VueUse Axios Unsplash API

Built in 2024 as my first project on Vue 3.

The Unsplash access key is bundled into the client, which is fine for a public demo and wrong for anything real.

There is no backend, no caching, and no error reporting beyond a retry button.

Next project

MCP Server