DEV Community

Evan Mercer
Evan Mercer

Posted on

Designing the Empty State When the Thing Users Want Usually Isn't There

Some features are empty most of the time by nature: a "live now" section, a sale banner, stories that expire after 24 hours. If the empty state is a blank area with "Nothing here", users read it as broken and leave. The fix is to treat "nothing right now" as the normal case, say so plainly, tell people when to come back if you know, and show the closest useful thing instead. Below is the pattern I use, with a small, framework-free implementation you can adapt.

Three kinds of "empty"

Lumping these together is the root of most bad empty states:

  1. Nothing yet, and that's expected. No live item right now; there will be later.
  2. Nothing, because of a limit. The content exists but can't be shown (private, region-locked, needs permission).
  3. Nothing, because something failed. Network error, timeout, bad input.

Each needs different wording and a different next step. Case 1 needs a fallback, case 2 needs an honest explanation, case 3 needs a retry.

Model it explicitly

Don't let the UI infer the state from items.length === 0. Return a status alongside the data:

// status: "live" | "none" | "restricted" | "error"
function viewState(result) {
  if (result.error) return { status: "error", message: result.error };
  if (result.restricted) return { status: "restricted" };
  if (result.liveItems.length === 0) return { status: "none", fallback: result.recentItems };
  return { status: "live", items: result.liveItems };
}
Enter fullscreen mode Exit fullscreen mode

Show time remaining, not just existence

When live items expire, the most useful single fact is how long each one has left. Round down to whole hours above one hour and show minutes below it:

function timeLeft(expiresAt, now = Date.now()) {
  const ms = new Date(expiresAt).getTime() - now;
  if (ms <= 0) return "expired";
  const mins = Math.floor(ms / 60000);
  if (mins < 60) return `${mins} min left`;
  return `${Math.floor(mins / 60)} h left`;
}

console.log(timeLeft(Date.now() + 5 * 3600e3 + 20 * 60e3)); // "5 h left"
console.log(timeLeft(Date.now() + 42 * 60e3));             // "42 min left"
console.log(timeLeft(Date.now() - 1000));                  // "expired"
Enter fullscreen mode Exit fullscreen mode

Compute it on the client from an absolute timestamp, so a tab left open for an hour doesn't keep showing a stale "3 h left". Refresh the label every minute.

Write the "none" state as information

Bad: No results.

Better: No story is live right now. Stories last 24 hours, so check back later. Here are the most recent videos in the meantime.

The better version says what's missing, that this is normal, why, and what's shown instead. Then render the fallback directly underneath, without making people click a tab.

Keep "restricted" and "error" honest

  • Restricted: say what the limit is and stop. "This account is private, so its posts aren't public." Don't hint that a workaround exists.
  • Error: say it's a problem on your side or the network's, keep the user's input in the box, and offer a retry button.

A real example

I use this pattern in TikStories, a free viewer for public TikTok profiles. Most public accounts have no active story at any given moment, so the "none" state is the common one: the page says no story is live, and the Videos tab already holds the account's recent posts, so there's still something to look at. When stories are live, each tile carries the hours left before it expires. Private accounts get a plain "this account is private" message, with no suggestion that anything can get around it.

Checklist

  • [ ] The API distinguishes none / restricted / error; the UI doesn't guess from an empty array.
  • [ ] The "none" message says it's normal and, if possible, when to come back.
  • [ ] A useful fallback renders in the same view.
  • [ ] Expiring items show time remaining computed from an absolute timestamp.
  • [ ] Restricted states don't imply a workaround.
  • [ ] Errors keep the user's input and offer a retry.

Top comments (0)