Empty and loading states

Every view that loads data has four states: loading, empty, error and ready. Design all four; switch between them below.

Loading members…

Loading

SituationUse
Content with a known shape (lists, cards, tables)Skeleton blocks that match the final layout
An action the user just triggeredButton loading on that button
Background work with no layout to holdSpinner size="sm" with a short label
Long, measurable work (uploads, imports)Progress with a value
  • Match the skeleton to the real content: same row height, avatar size and line count. The page should not jump when data arrives.
  • Skeletons are decorative. Hide them with aria-hidden and announce loading once with VisuallyHidden text inside an aria-live region, or aria-busy on the container.
  • Wait about 300ms before showing a loader for fast requests, so it does not flash.

Syncing 3 files…

Spinner has a built-in accessible label; pass label={null} when visible text next to it already says what is happening.

Empty

An empty state explains what will be here and offers the one action that fills it.

  • First use: icon, a title that names the thing ("No members yet"), one sentence of value, one primary action.
  • No results: no icon or action needed. Repeat the query and offer to clear filters: "No invoices match “fabrikam”."
  • Cleared / done: "You're all caught up" — no action.

Use variant="plain" inside a card or panel that already has a surface, and the default tray variant when the empty state fills a page region by itself.

Error

Use an Alert tone="danger" in place of the content, with a Retry action. Say what failed in plain words; do not show stack traces or status codes to end users. If part of the page loaded, keep it and show the error only where data is missing.