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="" or aria-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.
Anatomy of a first-use empty state Hierarchy: page heading, then the empty-state region containing a heading, an explanation, a primary action, an optional secondary link, and a decorative illustration hidden from assistive technology. Anatomy of a first-use empty statePage heading: Invoicesunchanged, so the page still has its h1Empty-state regionsection labelled by its headingHeading: No invoices yeth2, describes the stateExplanationwhat appears here and where it comes fromPrimary actionbutton "Create invoice"Secondary link"Import invoices from CSV"Illustrationalt="" — decorative
Every text element names the table's purpose; the illustration adds nothing to the accessibility tree.

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.

Placeholder rows versus an explanatory empty state Comparison: fake example rows are announced as real data and invite interaction; an explanatory state describes purpose and offers the real action. Placeholder rows versus an explanatory empty state✗ Fake example rows"Table, 4 rows" — none of them realUsers try to open example recordsExample data copied into exportsPurpose left to inference✓ Explanatory empty state"No invoices yet" headingOne clear action: Create invoiceNothing to export by mistakePurpose stated in a sentence

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
Choosing the right empty message Flow from cause to message: never had data shows first-use; filters exclude everything shows no results; load failed shows error; no permission shows access message. Choosing the right empty messageNever had data?first-use state"No invoices yet" + Create invoiceFilters exclude everything?no-results statekeep table; Clear filtersRequest failed?error staterole="alert"; Try againNo permission?access message"You do not have access" + requestaccess

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.

Permalink to "Related"

← Back to Empty, Error & No-Results States