First-Use Empty States for Data Tables
Permalink to "First-Use Empty States for Data Tables"A first-use empty state appears before there is any data: a new account’s invoice list, a project with no tasks, a report that has never run. It is different from a filtered empty state because nothing went wrong and nothing can be cleared — the user needs to understand what the table is for and how to put something in it. Done well, it is the most useful screen in onboarding. Done badly, a screen reader user hears an illustration description, a slogan, and a button labelled “Get started” with no idea what it starts.
This guide belongs to empty, error & no-results states. For the filtered case see accessible empty state for filtered tables.
Spec reference
Permalink to "Spec reference"- SC 2.4.6 Headings and Labels (AA) — the empty state’s heading and the action’s label describe purpose: “No invoices yet” and “Create invoice”, not “Nothing here” and “Get started”.
- SC 1.1.1 Non-text Content (A) — decorative illustrations get
alt=""oraria-hidden="true". - SC 2.4.4 Link Purpose (In Context) (A) — “Learn more” must be expanded: “Learn how invoicing works”.
- SC 1.3.1 Info and Relationships (A) — if a table skeleton is shown, it must not be exposed as a data table with fake rows.
- SC 4.1.3 Status Messages (AA) — when the first record appears and the empty state is replaced, announce the change.
When to use — and when not to
Permalink to "When to use — and when not to"Show a first-use state when the collection has never contained data for this user or workspace. Replace the table entirely here — unlike the filtered case, there are no columns worth showing yet, and an empty header row explains less than a sentence does.
Do not show it when data exists but the user lacks permission to see it; that is an access message (“You do not have access to invoices in this workspace”) with a different action. Do not show it for a failed load — see announcing data load errors in grids.
The misapplication: a fake table of placeholder rows (“Example Customer, $0.00”) presented as real data. Screen readers announce it as a table with rows, and users try to interact with records that do not exist.
Annotated code example
Permalink to "Annotated code example"<h1>Invoices</h1>
<!-- Region named by its heading (SC 1.3.1, 2.4.6) -->
<section class="empty-state" aria-labelledby="empty-heading">
<img src="/img/invoices-empty.svg" alt="" width="160" height="120"> <!-- decorative (SC 1.1.1) -->
<h2 id="empty-heading">No invoices yet</h2>
<p>Invoices you create or import will be listed here with their customer, status and amount.</p>
<!-- Specific action names (SC 2.4.6, 2.4.4) -->
<a class="btn" href="/invoices/new">Create invoice</a>
<a href="/invoices/import">Import invoices from CSV</a>
</section>
<p id="list-status" role="status" class="visually-hidden"></p>
// After the first invoice is created and the user returns to the list
function renderList(rows) {
if (rows.length && emptyStateVisible()) {
replaceEmptyStateWithTable(rows);
status.textContent = 'Invoice created. 1 invoice in the list.'; // SC 4.1.3
}
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected announcement | AT deviations |
|---|---|---|
| Page load | “Invoices, heading level 1” then on reading “No invoices yet, heading level 2” | — |
| Tab to primary action | “Create invoice, link” | Some products use a button; either is fine if it navigates or opens a dialog accordingly |
| H key navigation | Reaches “No invoices yet” | — |
| First record appears | “Invoice created. 1 invoice in the list.” | Announced after navigation completes |
Integration context
Permalink to "Integration context"First-use states often lead straight into data entry; when the action opens a dialog rather than a page, apply restoring focus after closing complex modals so focus lands on the new table after the dialog closes. If the import path is a CSV upload, its progress and errors are covered in error summaries for batch operation failures.
Gotchas
Permalink to "Gotchas"Duplicate headings. If the empty-state heading repeats the page heading (“Invoices” twice), the outline becomes confusing. Use the heading to describe the state.
Animated illustrations. Looping animations violate SC 2.2.2 if they run longer than five seconds without a pause control. Prefer static images or respect prefers-reduced-motion and stop after one cycle.
Empty states inside dashboards. A widget with no data still needs its own heading and a sentence; “No data” in grey text is not enough to explain what the widget would show.
Design system notes
Permalink to "Design system notes"Separate FirstUseEmptyState, NoResultsState, ErrorState and NoAccessState components with required props for heading, explanation and action. Required props stop the most common failure — the generic “Nothing here” — from being shipped by default.
Framework notes
Permalink to "Framework notes"Routing affects how the first-use state hands over to the real table. In Next.js and Remix, creating the first record usually redirects back to the list; pass a flash message (“Invoice created”) through the redirect and write it into the persistent status region after hydration. In Angular, use the router’s navigation end event and the CDK LiveAnnouncer. In Vue, a watch on the list length that fires when it goes from 0 to 1 is enough.
// Vue 3: announce the transition out of the first-use state exactly once
watch(() => invoices.value.length, (n, prev) => {
if (prev === 0 && n > 0) status.value = `Invoice created. ${n} invoice in the list.`;
});
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Should the first-use state be inside the table's caption region?
No table exists yet, so give the empty-state section its own heading. When the first row arrives, the real table with its caption replaces it.
Is "Get started" an acceptable button label?
Only when the surrounding text makes the purpose obvious and the action really is a multi-step onboarding. “Create invoice” is almost always clearer, both for screen reader users scanning by control and for voice control users, who must say the label.
Should illustrations ever have alt text?
Only if they convey information not in the text, which empty-state art rarely does. Decorative is the default.
Should the first-use state include a tutorial video?
Only as an optional link with a descriptive name, captions and a transcript. Autoplaying media in an empty state distracts every user and, with sound, interferes directly with screen readers.
How do I handle a workspace where other people will add the data?
Say so: “Invoices created by your team will appear here.” Offer the action the current user can take — invite a teammate, connect an integration — rather than a Create button they may not have permission to use.
Related
Permalink to "Related"- Empty state for filtered tables — the no-results case
- Zero-results suggestions — guiding users to data
- Landmarks and headings for dashboards — empty widgets in context