The Screens Nobody Mocks Up
By Atif Ullah · · 7 min read
On this page
Open any design portfolio and you'll see the same thing: perfectly populated dashboards, profile pictures that are all well-lit, names that are exactly the right length, charts trending gently upward. It's the product on its best day.
Real users rarely see the product on its best day. They see it on a slow train connection, with no data yet, after a failed payment, with a name that's thirty-four characters long and breaks the card layout. And in those moments, the quality of the experience is decided by screens that most designers never draw.
I've come to believe that the states we skip are the ones users remember most. Nobody tells a friend about a dashboard that loaded fine. Plenty of people tell friends about the app that ate their form data when the connection dropped.
The forgotten states#
For almost any meaningful component, there's a set of states that need deliberate design. I keep a short checklist taped (metaphorically) to my monitor:
- Empty — there's nothing here yet. First use, filtered to zero, everything cleared.
- Loading — data is on its way. Could be fast, could be slow.
- Partial — some data loaded, some didn't. Some fields are missing.
- Error — something went wrong. Network, server, permissions, validation.
- Overflow — far more data, or far longer text, than the mockup assumed.
- Offline — the connection is gone, and maybe it's coming back.
- Success — the thing worked, and the user needs to know it.
The ideal state — fully populated, everything working — is just one row in that list. Yet it routinely gets ninety percent of the design time.
Empty states are onboarding#
An empty state is the first thing a new user sees. It's the moment they're deciding whether this product is worth their time. And far too often, it's a gray illustration and the words "No data yet."
A good empty state does three things. It explains what this space is for. It tells the user how to fill it. And it gives them a direct way to do that, right there.
On a healthcare product I worked on, the patient list for a new clinic started empty. The original design showed an illustration of a clipboard. We replaced it with a short explanation, a primary button to import patients from a spreadsheet, and a secondary option to add one manually. Setup completion went up noticeably, and support tickets asking "how do I add patients?" largely went away.
It's also worth distinguishing between kinds of empty. "You haven't added anything yet" is very different from "your filters returned nothing" and from "you've completed everything — nice work." Each deserves its own message. Showing the onboarding prompt to someone who just filtered too aggressively is confusing; showing "no results" to someone who just cleared their task list misses a moment to make them feel good.
Loading should set expectations#
Loading states are about managing time and trust. The question isn't "how do we show that something is happening?" It's "how do we make the wait feel reasonable?"
A few principles I lean on:
- Skeletons over spinners for content that has a predictable shape. A skeleton tells the user what's coming and makes the eventual load feel faster because the layout doesn't jump.
- Don't show a spinner for very fast operations. A spinner that flashes for a split second is more distracting than a tiny delay. Wait a beat before showing it.
- Show progress for long operations. If something will take more than a few seconds — an upload, a report generation, a blockchain confirmation — tell the user what's happening and roughly how long it'll take.
- Keep the rest of the interface usable. Loading one panel shouldn't freeze the whole screen.
On a DeFi project, transactions could take anywhere from a few seconds to several minutes depending on network conditions. A generic spinner made people anxious enough to retry, which caused duplicate transactions. Replacing it with explicit steps — submitted, pending confirmation, confirmed — with plain-language explanations dramatically reduced the panic.
Errors are a conversation#
Error messages are where products reveal how they really feel about their users. "Something went wrong" is the interface equivalent of a shrug.
A good error message answers three questions: what happened, why it matters, and what the user can do next. In that order, in plain language.
Compare:
- "Error 402."
- "Payment failed."
- "Your card was declined, so your parking session hasn't started yet. Try another card, or check with your bank."
The third one is longer, but it's the only one that actually helps. It tells the user the important consequence — the session didn't start, so they might get a ticket — and gives them a path forward.
A few more things I try to get right:
- Preserve user input. If a form submission fails, never clear the form. Losing data is one of the fastest ways to lose trust.
- Place errors near their cause. A validation error belongs next to the field, not in a banner at the top of a long page.
- Don't blame the user. "You entered an invalid date" feels accusatory; "That date doesn't look right — try DD/MM/YYYY" feels like help.
- Distinguish recoverable from fatal. If retrying will probably work, offer a retry button. If it won't, say so and offer an alternative.
Partial and overflow: where layouts break#
Partial and overflow states are where pretty mockups go to die. Every design looks great when every name is "Sarah Chen" and every company has a logo.
I deliberately stress-test layouts with hostile data. Very long names. Names with no spaces. Missing avatars. Prices with seven digits. Twelve tags on an item designed for two. Lists with one item and lists with ten thousand.
It's not glamorous work, but it catches problems before developers do — and when developers find them, they usually have to invent a fix on the spot. Truncation rules, wrapping behavior, fallback images, and pagination are all design decisions. If I don't make them, someone else will, under time pressure.
Offline is a real context#
For mobile products especially, offline isn't an edge case. It's a Tuesday. Parking garages, elevators, rural roads, airplane mode.
At minimum, the product should tell people when they're offline and what will and won't work. Better still is designing for graceful degradation: show cached data with a clear "last updated" note, queue actions to sync later, and confirm when sync succeeds.
The worst outcome is silent failure — the user taps a button, nothing seems to happen, and they don't know whether their action went through. That uncertainty is far worse than an honest "you're offline, we'll send this when you reconnect."
Success deserves design too#
It's easy to forget that success is a state. When something works, the user needs clear confirmation — especially for actions with real consequences, like payments, bookings, or submissions.
Confirmation should be proportional. A saved preference might just need a subtle checkmark. A completed payment needs a clear, unambiguous confirmation with the key details and a receipt. A finished onboarding flow might deserve a small moment of celebration.
Making it a habit#
The trick to designing these states consistently isn't talent. It's process. I add a state inventory to every component and feature before I consider the design done. If a state doesn't need custom design, I write that down explicitly, so it's a decision rather than an omission.
It adds maybe twenty percent to the design time for a feature. In exchange, products feel solid, developers stop guessing, and the moments that used to generate support tickets quietly turn into moments that build trust.
The happy path is what we show in the portfolio. The other states are what users actually live with.