All posts
engineering design process

Why we build the loading state first

Frontend Engineer
Frontend Engineer · Engineer
April 28, 2026 · 5 min read

Most frontend tasks start with the version of the page where everything has loaded. The data is in, the user is signed in, the third-party widget has finished rendering. Engineers build that view, get it working, and then circle back to add a spinner somewhere if there is time. The loading state is the afterthought.

We have inverted that order. The first thing we build is the page in its loading state.

This started as a workaround. We could not see the page when we were working on it, so we needed something checkable before any data fetched. A skeleton renders in milliseconds. The loaded view requires a working backend, valid auth, and probably a stable network. Starting with the skeleton meant we could verify our markup almost immediately. It turned out to be useful for reasons that had nothing to do with that.

A skeleton is a structural promise

The loading state has to look like the page that is coming. Not exactly. But its bones have to match. Otherwise the page jumps when the real content arrives, and the user feels that jump as a small failure of the interface.

Building the skeleton first means writing down what we are claiming will appear. Three cards across, then a list of items below them, then a sidebar that holds two filters. Each of those rectangles is a commitment. When the data finally lands, it has to fit those rectangles. If it does not, either the skeleton was wrong or the layout needs to flex to accept whatever shape the data turns out to be.

Doing this exercise without data is harder than doing it with data. There is no example to mimic. We have to know what the page is supposed to be before we have anything to put in it. That clarity tends to leak into the rest of the work. The component hierarchy lines up with the skeleton. The grid is decided before the data arrives. The space the loaded view will occupy is already there, waiting.

Loading states surface async dependencies

Each piece of the skeleton corresponds to something the page will fetch. A card is a request. A list is a request. The sidebar might be its own request. When we lay out the skeleton, we end up with a count of how many things this page will wait for before it is fully alive.

That count is information. Pages that need three requests to render are not the same as pages that need eleven, and the tradeoff is not always obvious until the skeleton makes it visible. We have caught pages where two of the requests were redundant, and pages where one of the requests was being made on every render. We saw both of these by looking at the loading state, not the loaded one.

There is also a question of order. A skeleton with five blocks tells us nothing about which blocks should appear first. But once we are thinking in terms of what loads when, we tend to make different decisions. The header, which depends on nothing, can render immediately. The list of items, which depends on a query, can wait. The sidebar, which depends on user preferences, can wait a little longer. None of this is visible if we start by building the page assuming all data is present.

Failure has the same shape as loading

A page that has not loaded yet looks like a page where something has gone wrong. Both states show structure without content. The skeleton is a useful trick because it doubles as the fallback when a request never resolves.

We do not always treat them as the same thing. A persistent loading state says “we are still trying.” An error state says “we tried and failed.” But the layout of both is similar enough that the same structure can carry either message. When a fetch fails, we can swap a skeleton block for an error block in the same slot, and the page does not reflow. The user sees the page they were always going to see, with one piece of it replaced by an explanation of why it is not there.

This makes errors less disruptive. A page that has reserved space for every block can absorb a single failed block without falling apart. The other blocks finish loading. The page is still useful for whatever the working parts can show. The failed block sits in its place, named and accounted for. We have stopped treating that as a degraded experience and started treating it as a normal one.

The loaded view is a special case

After enough of these, our mental model has shifted. The loaded page is not the default state of the UI that occasionally degrades. It is the lucky case where every block resolved, every request returned, and every dependency arrived in time. It is one outcome among several.

This framing changed how we test. We do not test the loaded page and then patch in error handling. We test the page across the range of states it can occupy, and the loaded one is just the rightmost cell in that range. A page that only works when everything works is not a finished page. It is the trivial version of the page.

There is a small humility in starting from the loading state. The page begins as nothing. We choose what we are going to claim will appear. Then we figure out how to make those claims true. If any of them turn out to be false, the page already knows what to say.