Most component libraries are a catalogue of things: button, input, card, table, modal. Each with variants, each documented, each looking correct in the design file.
Then the application ships and the complaints are never about the button. They're about the screen that was blank for two seconds with no explanation, the form that appeared to save and didn't, the empty page a new user landed on with nothing to do. None of those are missing components. They're missing states — and states are what a catalogue organised by component quietly fails to make anyone think about.
The most useful thing I've done to a dashboard codebase is to stop asking "what components do we need" and start asking, for every screen that fetches anything, "what are its five states, and have we designed all of them".
1. Loading, and the trap of doing it well
Everyone has a loading state. Most are worse than they need to be in the same specific way: they replace the layout instead of occupying it.
A centred spinner where a table should be means the page has no height, so the footer rides up, and when the data arrives everything jumps. A skeleton that matches the real layout's dimensions costs the same to build and holds the page still.
Then the distinction that matters more: first load is not refetch. First load has nothing to show, so a skeleton is right. A refetch — someone changed a filter, something revalidated — already has perfectly good data on screen, and throwing it away to show a skeleton is strictly worse than leaving it and dimming it. Treat both as "loading" and every filter change makes the page flash.
One more: don't show a loading state for something that usually takes 80ms. The spinner appears and vanishes and reads as a flicker. Delay it by a couple of hundred milliseconds so fast responses never show one at all.
2. Empty, which is really three states
The most-skipped state and the one with the most product value in it, because it's what a new user sees first and it's usually the only screen you get to teach them anything.
Three genuinely different situations get collapsed into one "No data" every time:
- Nothing yet. A new account with no orders. This is an opportunity — explain what will appear here and give them the button that creates the first one.
- Nothing matching. They filtered and nothing came back. Completely different: the data exists. Say what's being filtered and offer to clear it. Otherwise a working filter reads as a broken page, which is the single most common false bug report in any admin tool.
- Nothing permitted. There is data, and this user may not see it. Say so, rather than implying the system is empty.
Same visual component, three different messages and three different actions. If your empty state takes no props, it's wrong.
3. Error, which has to be actionable and honest
"Something went wrong" is the industry standard and it is close to useless, because it tells the user neither what to do nor whether it was their fault.
What a person needs is: is this me or you, will trying again help, and what do I do if it doesn't. That means distinguishing a validation failure (their input, fixable, be specific) from a permission failure (not fixable by retrying, say who to ask) from a server or network failure (probably temporary, give them the retry button rather than asking them to reload).
Put the retry in the error state. An error message with no action is a dead end, and the user's only remaining option is a full page reload, which loses everything else they were doing.
And don't leak internals. A stack trace or a raw database error in the interface is a security matter as much as a design one — log the detail, show the person something they can act on, and put a correlation ID in both so a support conversation can connect them.
4. Partial, the one that isn't in any design file
This is the state I'd argue hardest for, because it's the one that produces bugs rather than merely looking unfinished.
Real screens assemble several sources. A dashboard loads the user, their stats, a chart and a notification count. In the design file all four are present. In production one endpoint is slow, or down, or returns something unexpected.
If you've only designed all-or-nothing, you get one of two bad outcomes: the entire page is held hostage by the slowest widget, or the failing section renders as a blank gap that looks like a rendering bug and gets reported as one.
Decide per section which is essential and which is supplementary. If the notification count fails, the dashboard is still a dashboard — show it with an inline "couldn't load" and a retry in that corner only. If the user's identity fails to load, nothing on the page is trustworthy and you should say so at the page level. That's a product decision, made once per screen, and it doesn't happen unless someone asks the question.
5. Success, which is not the absence of failure
After a mutation, the user needs to know it took. Silence is the same experience as a failure — they press save, nothing visibly changes, and they press it again.
Prefer showing the changed data over showing a toast about it. A toast that says "Saved" while the row still displays the old value is actively confusing, and toasts are missed by anyone who happened to be looking elsewhere. If the result is visible, that is the confirmation.
If you do use optimistic updates, design the rollback before you ship them. The failure case — the change appeared, then silently reverted — is worse than never having been optimistic, because the user believes it saved and has moved on.
Build them as one component, not five
The reason teams skip these isn't ignorance. It's that doing it per screen is five extra branches every time, so it survives about three screens and then someone in a hurry returns null and it's never revisited.
So make it structural. One wrapper that takes the request status and the data and decides which state to render, and per-screen you supply the empty message and the error copy. Now the default path handles all five, and skipping one requires effort rather than being what happens when you're busy — the same principle that makes scoped queries work for permissions: the safe thing has to be the easy thing.
The test for whether a component library is finished isn't whether the button has enough variants. It's whether a developer building a new screen can get all five states without thinking about it.