Loading, empty, and error states — designing for the whole lifecycle
A design mockup usually shows one thing: a screen full of realistic-looking data, working perfectly. Real screens spend meaningful time loading, occasionally have nothing to show, and sometimes fail outright — and each of those states needs to be designed on purpose, not left as an afterthought.
Intermediate
3 min read
The happy path is one state out of at least four
Every real screen that loads data has at minimum four distinct states to
design for, not just the one usually shown in a mockup:
Designing (and building) only the "loaded, full of data" state and leaving the
other three to whatever happens by default is exactly how you end up with a
blank white screen on a slow connection, or a confusing empty rectangle where
a list should be.
Loading states: never leave a blank screen
A screen with no visible content while data loads reads as broken, not "in
progress" — a user has no way to distinguish "this is loading" from "this
crashed." A spinner is the minimum; a skeleton screen (a gray, roughly
content-shaped placeholder matching the eventual layout) is usually better,
since it sets an accurate expectation of what's about to appear and feels
faster even at the same actual loading time, because the layout doesn't
suddenly shift once real content arrives.
Loading skeleton (approximates final layout):
┌────────────────────────┐
│ ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░ │ <- title-shaped bar
│ ▓▓▓▓░░░░░░░░░░░░░░░░░░ │ <- shorter subtitle-shaped bar
│ ░░░░░░░░░░░░░░░░░░░░░░ │
└────────────────────────┘
Empty states: an opportunity, not just an absence
An empty state — a list with zero items, search results with no matches — is
often left as literally nothing: a blank area with no explanation. A good
empty state instead explains why it's empty and, where relevant, what to do
about it: "No results for 'xyz' — try a different search" is meaningfully more
useful than an empty box, and "You haven't added any projects yet — create
your first one" turns a dead end into a clear next action rather than a
confusing void.
Error states: explain what happened, don't just fail silently
An action that fails with no visible indication — a request that errors out
with nothing shown on screen — leaves a user unsure whether anything happened
at all, directly connecting to the previous lesson's feedback principle. A
real error state names what went wrong in plain language and, where possible,
offers a concrete next step: "Couldn't load your data — check your connection
and try again" with an actual retry button, rather than a generic, dead-end
"Something went wrong."
❌ [blank screen, no explanation]
✅ "Couldn't load your projects.
Check your connection and try again."
[ Retry ]
Designing all four states before writing the implementation
The practical discipline this lesson argues for: when building any screen that
loads data, explicitly design (even just sketch, or write out) all four states
before writing the code — not just the happy path, with the other three
handled reactively once someone notices they're missing. This is directly
analogous to the Software Engineering Fundamentals domain's point about
designing for the whole lifecycle of a system, not just the initial build —
applied here to a single screen's actual range of real conditions.