Skip to content
Component installs need the registry setup
VegaStack Design

Empty states

The ten-tier empty-state system — when a surface has nothing to show, found nothing, or could not load, which tier fits, and the shared icon language.

Last updated

An empty state is a design surface, not an accident. VegaStack ships a graded system so every "nothing here yet" reads as intentional and consistent — from a full-page welcome down to a single blank field. Pick the lowest tier that carries the meaning; reaching for a big illustration where a faint phrase would do is the most common mistake.

The ten tiers

#TierComponentUse it for
1PageEmpty + EmptyMedia variant="icon" + a CTAa whole view with no records ("No companies yet")
2PanelEmpty inside a Carda card/pane with no content (a sidebar list, a widget)
3Inline valuea muted placeholder spana single blank value slot ("No value", "Set a value…")
4StatStatEmptya metric with nothing to report ("No connection")
5WellEmpty className="border" (dashed)a favourites well — a drop target for files is Dropzone, not this tier
6Blocked / errorEmpty + an icon in text-destructive-texta state the user must resolve ("No mailbox configured")
7Not foundEmpty + a SearchX icona thing that doesn't exist (404, deleted record)
8Loading→emptySkeleton → tier 1/2the settled empty after a load resolves to nothing
9No matchesEmpty + a SearchX icon + a "Clear filters" actiona search or filter that matched nothing, while the collection itself exists
10Couldn't loadEmpty role="alert" + an icon in text-destructive-text + "Try again"a load that failed ("Couldn't load tasks")

Blocked or failed. Tier 6 is a state the user must fix (configure, connect, get permission); tier 10 is a load that failed and may work on a retry. Either one carries role="alert" when it replaces content after a failure, so the failure is announced; a blocked state present from first paint stays silent.

No matches is not empty. Tier 1 says "there is nothing yet, create one"; tier 9 says "there is something, but not for this query", so its one action clears the query rather than creating a record:

<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <SearchXIcon className="text-muted-foreground" />
    </EmptyMedia>
    <EmptyTitle>No matches</EmptyTitle>
    <EmptyDescription>
      Try a different search or clear the filters.
    </EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button variant="outline" onClick={clearFilters}>
      Clear filters
    </Button>
  </EmptyContent>
</Empty>

Couldn't load keeps what the user typed and offers the retry in place:

<Empty role="alert">
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <CircleAlertIcon className="text-destructive-text" />
    </EmptyMedia>
    <EmptyTitle>Couldn't load tasks</EmptyTitle>
    <EmptyDescription>Check your connection, then try again.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button variant="outline" onClick={retry}>
      Try again
    </Button>
  </EmptyContent>
</Empty>

One drawing language

Empty is upstream's component, and it has no illustration part: the media slot is EmptyMedia, and what goes in it is a lucide icon. That keeps one drawing language without shipping a second asset set:

  • themes need no variants — a lucide glyph is pure currentColor, so it tracks foreground/muted-foreground;
  • the error tier is just a colour — text-destructive-text on the icon, no second asset;
  • EmptyMedia variant="icon" gives the glyph its rounded muted plate, which is what makes the tiers read as one family across every surface.
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <ClipboardListIcon className="text-muted-foreground" />
    </EmptyMedia>
    <EmptyTitle>No tasks yet</EmptyTitle>
    <EmptyDescription>Create your first task to get started.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>New task</Button>
  </EmptyContent>
</Empty>

Rules

  • Never leave a data cell truly blank. A blank cell reads as broken; a muted placeholder span or StatEmpty says "empty on purpose" in the text-muted-foreground register.
  • The title carries the meaning; the icon is decorative (aria-hidden).
  • One CTA. An empty state proposes the single next action — not a menu.
  • Blocked ≠ empty. If the user must do something to unblock (configure, connect, permission), use the error tier in the destructive register — not the neutral one.
  • Default copy. A retry is "Try again". A loading line is Loading {noun}…, with the ellipsis character. The end of a list is "End of list". A one-line state has no full stop; a sentence does. Every default uses EmptyMedia variant="icon".

On this page