Skip to content
Contact
All writing

· 6 min read

A state checklist for business software

Empty, zero, loading, partial, denied, expired, interrupted: the states I check on every screen before I call a design done.

UX designDesign systemsChecklistby Bibhushan Saakha

Before I call a screen done, I check it against a list of states: empty, zero, loading, partial, error, denied, expired, interrupted and irreversible. Business software is used on bad days: with half the data loaded, by someone without permission, after the connection dropped. A design that only covers the full, happy screen is a third of a design. This post is the UI states checklist I use, with an example of each from work I've shipped or handed off.

I'm the only UI/UX designer at Tigg, an accounting and POS product for businesses in Nepal, and I build much of the frontend too. That means I meet the missing states twice: once in Figma, and again in the browser when the real data arrives. The checklist exists so I meet them the first time.

Why design for edge cases and states at all?

Because the happy path is the state people spend the least time worrying about. They remember the moment they got stuck: the code that didn't arrive, the backup that "failed" while the server was still working, the error that pointed to row 22 of a file they no longer had open.

Every state below needs three things:

  1. Wording that says what is true right now.
  2. A way forward or back, never a dead end.
  3. A rule for how the state is entered and left, so engineering and QA can test it.

If I can't write all three, the state isn't designed yet.

The UI states checklist

StateQuestion to askOne-line test
Empty (first use)Does it teach the next step?There's a sentence and an action, not a blank box
Empty (filtered)Does it say the filter caused it?"No records found" sits beside a way to clear the filter
ZeroIs a confirmed 0 different from blank?A blank can't be saved as 0
Loading / waitingDoes the person know what they're waiting for?Buttons are disabled while a request runs; the wait has a name
PartialDoes one failure blank the whole screen?Only the failed part retries
ErrorIs the error next to its cause, with a fix?The message names the field, row or cell
DeniedCan someone learn why they can't act?A missing permission is explained, not just absent
ExpiredIs old access ended clearly?The expired link says so, and offers a way back
InterruptedWhat happens on reload, leave or disconnect?The task resumes or is reported, never silently lost
Invalid transitionIs the blocked step explained where it happens?The person stays put and sees why
IrreversibleDoes the confirmation repeat what will be lost?It names the item and says it can't be undone
Switched offWhat if a setting removes this feature?Filters, fields and permissions tied to it disappear cleanly

The rest of this post walks through the ones teams skip most often.

Empty states: three kinds, three messages

"Empty" is not one state. In Tigg's Bulk Import design there are at least three: "No import history" in recent imports (nothing has happened yet), "No fields mapped yet" on the mapping page (the task has started), and "No records found" in a filtered grid (the data exists but the filter hides it). Each needs different words, and only the last needs a clear-filter control.

Sometimes empty is a perfectly normal state, not a gap. In the Myra concept, a woman with no companion linked sees "No one is following your cycle. That's fine." Using the app alone is the default, so the screen shouldn't nag her to fill it.

Empty vs zero: the state that breaks money

In a POS cash count, a blank field and a 0 mean opposite things. Blank means the cashier forgot to enter something; 0 means there is no cash. In the cash sessions flow, a blank opening count is blocked ("Enter an opening amount."), while a 0 is accepted and stays visible, so the cashier can see what they confirmed. A cash-in of 0 is blocked for a different reason: a movement of nothing isn't a movement.

I wrote a whole post on this: empty is not zero. The short rule: never let an input turn a blank into 0 on the way to the server.

Loading and waiting: give invisible waits a name

The worst waits are the ones the person can't see. In the sign-in flow, the browser needs a moment to identify the device before verification can work. Until it has, the button reads "Preparing..." and can't be pressed, so an early click doesn't fail with a confusing error. The emailed code is the second invisible wait, so the code screen says where the code went and shows "Resend in 00:45".

For long jobs, a spinner is a promise you can't keep. A full company backup can take minutes, and the page can't honestly say how far along it is for much of that time. The lesson I took from it: show that work is happening without claiming how much.

Partial and error states: fail small, and near the cause

A screen built from several requests can fail in pieces. On the cash drawer card, if some details fail to load, the card says "Couldn't load some cash drawer details." with Retry, and only the failed parts are fetched again. One slow request shouldn't blank the whole card.

Errors belong next to their cause. A wrong verification code clears all six boxes and puts the cursor back in the first one. A serial number the server refuses shows its reason under the serial field, and is never added to the line.

Empty(0/N) red · autofocus Checkinginput + button disabled Added (k/N)chip · focus returns Complete (N/N)green · focus → Batch Enter valid k = N next scan while k < N Transport failure"please try again" Rejectedserver message shown Duplicatelocal check, no call Limit reachedinput disabled at N network error refused Enter, already on line k ≥ N Bad requestnot shown to cashier request fault Rejected entries are not added to the line. "Invalid" and "could not check" are different states. IMPOSED FROM OUTSIDE THE FIELD Warehouse loading"try again in a moment" Blocked (denied)no default warehouse Incomplete after save"Add m more…" Overfilledquantity lowered later
DiagramThe serial-number field in the POS has thirteen states: a scan loop of four, four rejections, and five set from outside the field. Rejected entries are never added to the line.

Denied: hiding is not explaining

On the cash drawer card, people who don't own a session and aren't admins can see the drawer, but the edit and close controls aren't shown. Hiding keeps the card readable for the common case, a cashier looking at their own drawer. The cost is that a colleague looking at someone else's session gets no reason. A line like "Only the session owner or an admin can change this drawer" fixes that. It's on my list of next changes, and I mention it because a checklist is only useful if you admit where your own screens miss it.

A related trap is confusing a switched-off feature with a missing permission. When I designed a permission editor, I wrote copy to separate them: "This is an organization setting, not a permission on the user you are editing."

Expired and interrupted: design around leaving

People reload, switch tabs, close laptops and lose the connection. Assume all of it.

  • Interrupted. The backup page remembers the running job, and returning shows "Reconnecting to backup status…". Bulk import saves the grid as a draft when you leave, and unfinished imports can be reopened.
  • Expired. An old backup link says "This backup link has expired." with a way back. Backups expire after five days, and the page says so above the history table, because rows that vanish without warning look like lost data.
  • Expired stock. In batch and serial tracking, choosing an expired batch is held and confirmed ("Batch Already Expired"), and cancelling restores the previous value, so the screen never shows a batch the line doesn't have.
Loading historyon mount Idleempty · with history Starting"Preparing backup…" Start failedtoast, pointer cleared History errortoast, empty table Resuming"Reconnecting…" Pollingnext in 3 · 10 · 30 s Transient failuren = 1…4, invisible Job not foundrare ending Completed100%, toast, refetch Failedfailed, or any part failed Gave upjob may still run ok error Backup Now error job id, +1 s 3 s mount + same org error success, n=0 n = 5 failed completed "job not found" Every terminal state clears the stored pointer and returns to Idle. Unmount clears all six timers but keeps the pointer, so a later visit resumes. If a request cannot be made, or the start returns no job, the page goes back to Idle. dashed = rarer endings; next refinement is a clearer message
DiagramThe backup page's states as shipped. Transient failures are hidden on purpose; the dashed states are rarer endings that need clearer messages.

Invalid transitions and irreversible actions

When a step is blocked, keep the person where they are and say why. The mapping page in Bulk Import blocks Next while required fields are unmapped, turns those selects red and names them in the message. In the POS, saving an order with an incomplete serial line opens that exact line with focus in the serial field, instead of showing a toast that says "something is missing".

For irreversible actions, repeat what will be lost. Deleting a backup names it ("Backup from 12-03-2026 10:14:05"), because rows look alike. Posting an import says it "will create actual transactions and cannot be undone".

How I use the checklist in practice

  • In Figma, each state that a person can actually reach gets its own frame with its own name. "Required still unmapped but clicked next" is a real frame name, and it was the most useful frame on its page.
  • At handoff, I list states with how they're entered and left. Engineers can build from a table; they can't build from "handle errors nicely".
  • In QA, the table becomes the test list.
  • Afterwards, I write down the states I missed. That's why each lead case study on this site has a chapter on states and edge cases.

I don't run every screen through every row. A settings page rarely has an "expired" state. But asking the question takes seconds, and the answer is often "yes, and we didn't draw it".

References

questions people ask

Frequently asked questions

What UI states should every screen be designed for?

At minimum: empty, zero, loading or waiting, partial, error, denied, expired, interrupted and irreversible. Each needs its own wording and a way forward, not only the ideal state with full data.

What is the difference between an empty state and a zero state?

Empty means nothing has been entered or exists yet; zero is a real value someone confirmed. In a cash count, a blank field means the cashier forgot, while 0 means there is no cash, so the two must look and behave differently.

Should a missing permission hide or disable a control?

Either can work, but the person should be able to learn why they can't act. Hiding keeps a screen readable for the common case; a one-line explanation covers the person who expected to act.

How do you design for interrupted tasks in web apps?

Assume people will reload, leave or lose the connection. Save the task's state, show a resuming message when they return, and give long jobs a second way to finish, such as an emailed link.