How to Design Empty, Error, and Loading States Together
Three states, one screen, and a design review that will decide whether the product feels considered or improvised. Empty, error, and loading are usually drawn in three different weeks by three different people, each solving a separate ticket. The empty state gets a friendly illustration. The error state gets a red icon and an apology. The loading state gets a spinner pulled from a library. Shipped together, they read like three unrelated products stitched into one frame.
The fix is not a shared component library, although that helps. It is a shared premise: every one of these screens is a moment when the interface has to explain itself. The data has not arrived, or it will not arrive, or there is nothing to show. In all three cases the user is staring at absence and waiting to learn what happens next. Design them as a set and they start to feel like one voice. Design them apart and the seams show.
All three states are conversations about missing data
An empty inbox is not a failure. A failed payment is not an empty inbox. A list that is still fetching rows is neither. The temptation is to treat them as separate genres with separate tones: cheerful for empty, contrite for error, neutral for loading. That split is where most inconsistency begins.
A useful reframe: each state answers the same three questions. What is happening? Why? What can be done right now? An empty state that says only "No results found" answers the first and skips the other two. An error state that says "Something went wrong" fails all three. A loading state that shows a spinner answers none of them until it finishes.
Once the three questions become the brief, the states converge naturally. Copy length aligns. Visual weight aligns. The user learns one grammar across the whole product instead of re-learning it at every screen.
Loading states fail when they pretend the wait is short
Spinners communicate uncertainty. They say: something is happening, duration unknown. That is honest for a two-second fetch and dishonest for a thirty-second one. The longer the wait, the more the interface owes the user in structure.
Skeleton screens, the grey placeholder shapes that mimic the layout of incoming content, work well when the shape of the result is predictable. A feed of cards, a table of rows, a product grid. They set expectations about what will appear, which lowers the cost of waiting. They misfire when the result is unpredictable, because the skeleton promises a shape the data may not deliver.
Progress bars are a different promise: measurable completion. They belong to uploads, exports, multi-step imports, anything where the system genuinely knows how far along it is. A progress bar that jumps from 12 percent to 90 percent and stalls is worse than a spinner, because it makes a specific claim and then breaks it.
The most underrated loading pattern is the plain sentence. "Fetching the last twelve months of invoices." Short, specific, and it tells the user what is being computed and roughly how much. It also gives the interface somewhere to put a cancel button, which long operations should always offer.
The empty state has to distinguish blank from broken
There are at least three kinds of empty. The first is a new account with no data yet: a blank slate. The second is a filter or search that returned nothing: a dead end. The third is data that should exist but does not load because of permissions or a backend fault: a silent failure.
These need different treatments. A blank slate should show what the screen will look like once populated, plus the single action that starts filling it. A dead end should echo the query back and offer a way to widen it: clear filters, broaden the date range, search a different field. A silent failure should be an error state, not an empty state, and treating it as empty is the most common bug in the whole trio.
A dashboard that says "No transactions yet" when the user actually has 4,000 transactions and lost access to them is a lie dressed as an empty state. The cost is trust. The user files a support ticket, the support team discovers a permissions change, and everyone spends a day on something a correct state would have surfaced in one line.
Rule of thumb: if the system cannot distinguish empty from unavailable, it should assume unavailable and say so.
Error copy should name the failure and the exit
Most error messages are written by engineers for engineers, then softened by a content team until they say nothing. The result is the generic apology: "Oops, something went wrong. Please try again." That sentence contains zero information. It does not say what failed, whether the user's data is safe, or whether retrying will help.
Better error copy has two parts: a plain description of the failure and a concrete next step. "The file could not be uploaded because it is larger than 25 MB. Compress it or split it into two files." "The connection dropped before the payment was confirmed. Check the order history before retrying, so a duplicate is not charged." Each sentence tells the user what happened and what to do.
The retry button deserves scrutiny. A retry that calls the same failing endpoint with the same inputs will fail the same way. If the failure is transient, say so and offer retry. If the failure is structural (bad input, missing permission, expired session), the button should route to the fix, not to the same wall.
Error states also need a hierarchy. A field-level error belongs inline, next to the field. A form-level error belongs at the top of the form with a link to the offending field. A page-level error replaces the content area with a message and a recovery path. A full-app error takes over the screen only when nothing else can function. Using the heaviest treatment for the lightest problem is a common tell of unconsidered design.
Three states, one grid, one type scale
The visual layer is where consistency either happens or quietly falls apart. Empty, error, and loading screens sit in the same content region, so they should occupy the same box. Same max width. Same vertical centering. Same padding from the edges. If the empty state is a narrow column of centered text and the error state is a full-bleed banner, the product wobbles every time the user hits a different failure.
Type scales matter here more than they sound. A loading skeleton that uses a different font size from the content it replaces causes a visible jump when the data lands. An empty state headline at 28 pixels followed by an error headline at 18 pixels reads as two different products. Pick one heading size and one body size for all three states and hold the line.
Illustration is the third variable. A cheerful cartoon for empty and a red triangle for error creates a tonal whiplash. Either commit to illustration across all three or commit to none. The middle path, a small set of line icons in a consistent weight, usually ages better than character art, which dates fast and costs a redesign every eighteen months.
Timing decides which state the user actually sees
A spinner that flashes for 200 milliseconds is worse than no spinner at all, because it draws the eye to a state change that resolves before the eye finishes moving. Most interfaces should delay the loading indicator by a few hundred milliseconds so fast responses skip it entirely. Slow responses cross the threshold and get the full treatment.
The same logic applies on the way out. When a load fails after a long wait, the error state should acknowledge the wait: "This took longer than expected and then failed." A user who waited forty seconds for a spinner deserves a sentence that recognizes the forty seconds, not a flat "Something went wrong."
There is also a question of what happens to the rest of the screen. If only one card is loading, the rest of the page should stay interactive. Replacing the whole page with a skeleton because one widget is slow is a self-inflicted wound. Component-level states, scoped to the region that is actually in flux, keep the rest of the product alive while one part catches up.
Writing the three states in one sitting
The practical fix is procedural. When a feature is specced, the empty, error, and loading states get written in the same document, by the same person, on the same day. Not after launch. Not as a follow-up ticket. The states are drafted alongside the happy path because they are the happy path for a meaningful share of sessions.
A simple exercise: for any new screen, write the three states as three sentences a colleague could read aloud. What does the screen say when there is nothing? What does it say when the request fails? What does it say while the request is in flight? If any of the three sentences is vague, the state is vague. Rewrite it until all three could plausibly come from the same narrator.
That narrator is the product's voice, and the states are where it gets tested hardest. A marketing page can sound like anything. A loading screen after a failed payment is where the product either sounds like a person who knows what is going on, or like three teams who never met.
The screen that reads best in a design review is the one with data in it. The screens that determine whether the product is trusted are the ones with nothing in them at all.

