Most data tables ship with a single empty state: a centered "No data" message, sometimes with a small illustration, sometimes with a "create new" button. It looks fine in design review. It produces support tickets in production.
The problem is that "the table is empty" can mean three completely different things, and the user's right next action is different in each case. A single generic message hides that distinction and leaves the user guessing.
The three cases are: first-use (no data exists yet), filtered-empty (data exists but the current filters exclude everything), and load-failure (the request to fetch data did not succeed). Each one needs its own copy, its own actionable element, and its own visual treatment. Designing them all the same is one of the more common UX failures in data-heavy tools.
Case 1: First-use empty
The user has just landed on a screen where no data exists yet, because no one on the team has created anything that would appear here.
The user's question: "What goes here, and how do I make it?"
The right response: an explanation of what would appear in the table, plus a primary call to action for creating the first item, plus optional secondary actions for related onboarding paths.
A good first-use empty state for a "Customer accounts" table:
No customer accounts yet.
Customer accounts let you track your client relationships and link them to invoices and notes.
The copy explains the purpose of the table in one sentence. The primary action is the most common path forward. The secondary action acknowledges an alternative path (importing from another system).
The mistake to avoid: a generic "No data" message in the first-use case. The user does not know what data should be there or how to create it. The message is a dead end.
The Nielsen Norman Group has consistently reported that first-use empty states are one of the highest-leverage moments in onboarding new users to a tool: a good empty state can compress the time-to-first-value from days to minutes. A bad one extends it indefinitely.
Case 2: Filtered-empty
Data exists in the table, but the user has applied filters that exclude all of it. The "no results matching" case.
The user's question: "Why is nothing showing? Did the tool break?"
The right response: an explicit message that filters are active and excluding all rows, plus an action to clear or modify the filters.
A good filtered-empty state:
No accounts match the current filters.
You're filtering for: Status = Closed AND Created after 2025-01-01.
The copy names the filters that are active. This is the most common cause of "why is the table empty" support tickets, and surfacing the cause in the empty state eliminates most of them.
The mistake to avoid: a generic "No data" message that does not mention filters. The user has forgotten which filters they applied (or has navigated back to a screen that preserved the filters from earlier) and assumes the data is gone. They click around, they file a ticket, they have a bad experience that was entirely preventable.
A subtler version of this failure: showing the filter chips above the empty state but not in the empty state itself. The filter chips become visual noise above an empty area; the user is staring at the empty area, not at the chips. The empty state needs to surface the filter status itself.
Case 3: Load-failure
The fetch to load data failed. The server returned an error, the network is down, the auth token expired, the API rate-limited the user.
The user's question: "Is this temporary? Should I try again or do I need to do something?"
The right response: an explicit message that loading failed, plus context about the failure (when it happened, what error code, how to report it), plus a retry action.
A good load-failure empty state:
Could not load accounts.
Last attempt: 2 minutes ago. The server returned a 503 error, which usually means the service is temporarily unavailable.
Retry
If the problem persists, contact support and reference error code REF-7281.
The copy distinguishes the failure mode (503 versus 401 versus network error versus timeout) when possible. The user can self-diagnose whether to retry or to act on the underlying cause (re-authenticate, contact support, wait).
The mistake to avoid: showing the same "No data" message for load-failure as for the other cases. The user assumes the data is empty, makes wrong decisions based on that assumption, and may discover hours later that the load was failing silently.
The Web Content Accessibility Guidelines cover error message standards that apply to load-failure cases: the message should be perceivable, the cause should be identifiable, and the user should be able to recover. Generic messages fail all three.
Why teams design only one empty state
A few patterns I have observed repeatedly:
Empty states get designed late. The table is built, data flows in during development, and no one notices that the empty case has not been designed until QA or production. By then, the team is shipping something and reaches for a generic placeholder.
Empty states get designed during design review, when the table has demo data. The designer adds an empty state, the team reviews it in the context of the demo data, and the three different empty cases never come up because the data is there.
Empty states are seen as edge cases. The team thinks empty states are uncommon and not worth the polish. They are wrong: filtered-empty happens routinely in production, load-failure happens whenever the network has a hiccup, first-use happens for every new user the tool ever onboards.
Empty states are conflated with placeholder states. A "skeleton loader" while data loads is different from an empty state after data has loaded. Teams sometimes design only the skeleton and forget that the post-load empty case still needs its own treatment.
What good empty state coverage looks like
A practical checklist for empty state design in a data-heavy tool:
First-use empty: explains the table's purpose, primary CTA to create the first item, secondary CTA for alternative paths (import, link existing data).
Filtered-empty: names the active filters in the empty message, primary CTA to clear filters, secondary CTA to edit filters.
Load-failure: distinguishes the failure type when possible, includes a timestamp, primary CTA to retry, optional secondary CTA to contact support with an error reference.
Partial-load: when some rows loaded but the request was incomplete or paginated and the next page failed. Shows what loaded plus a load-failure message for the missing portion. This is a sub-case of load-failure but worth designing separately.
Permission-denied: when the user is authenticated but does not have access to view the data. Different from load-failure: the right action is to request access, not to retry. This is sometimes overlooked.
The pattern at 137Foundry for clients building data-heavy interfaces is to surface all five cases in design review explicitly, before the table goes to QA. The cost of designing them separately is small; the cost of shipping a generic placeholder is paid forever in support volume. Designers and developers benefit from looking at all five together because the cases share visual treatment but differ in copy and action.
A practical implementation note
The three (or five) empty states are usually conditional renders in the table component. A common implementation pattern:
function TableBody({ data, loading, error, filters }) {
if (loading) return <SkeletonLoader />;
if (error) return <LoadFailureState error={error} onRetry={refetch} />;
if (data.length === 0 && hasActiveFilters(filters)) {
return <FilteredEmptyState filters={filters} onClear={clearFilters} />;
}
if (data.length === 0) {
return <FirstUseEmptyState />;
}
return <DataRows data={data} />;
}
The conditional structure makes it clear which case is which, and prevents the "fall through to generic empty" failure that happens when there is only one empty state component for all cases.
The Mozilla Developer Network covers the underlying fetch and error-handling patterns that the load-failure case needs to detect specifically. Building empty states well is partly a design question and partly a question of having reliable error context to display, which depends on the data-fetching layer surfacing useful errors.
What the user experiences when this is done right
A user encountering a first-use table sees a welcoming explanation of what goes there and a clear next action. They create the first item and the table populates.
A user who filters to an empty state sees an explicit "filters are excluding everything" message and clears the filters. They never wonder if the tool is broken.
A user who hits a load-failure sees a clear failure message with a retry button and an error reference. They retry, the load succeeds, they continue. If retries keep failing, they have a reference to give support.
In all three cases, the time the user spends being confused is short, the next action is clear, and the support tickets do not get filed. That is what good empty state coverage actually produces.
The longer treatment of data table design at scale covers how empty states pair with the rest of the design decisions in a production data table.
Top comments (0)