Patterns

Empty states

What to show in a slot that has no data yet — a blank space is a missed chance to help. Keep it friendly and direct: say what the slot is waiting for, then point at the next step. No apology, no blame — an empty state is a welcome mat, not an error.

This recipe is Beta. The composition and the copy formula below are settled; the per-feature copy library and the illustration treatment policy are pending and will be authored as the patterns appear in product.

Anatomy

The empty slot is a centered column inside whatever container owns the data — usually a Card. Four parts, in order; only the heading is mandatory.

text
container (the empty slot — a Card, a table body, a panel)
  Icon      decorative, aria-hidden (optional)
  heading   system voice — names what's missing
  body      one sentence — the next action
  Button    the one action (optional for filtered-empty)

The heading speaks in the system's slab-serif display voice — flat type on a flat, friendly surface (the engraved treatment belongs to the proposed system). The body is one warm sentence: the next step. One action is enough; don't list five options.

First-run empty

Nothing here yet because the user hasn't created anything — which makes this the friendliest moment in the product. Name what the slot is waiting for and hand over the first step.

First-run

No evidence attached

Upload a file or link a system to get started.

A Card-owned slot: a friendly icon on its color disc, a display-voice heading naming the gap, one sentence, one action.

Filtered to empty

Data exists, but the active filters exclude all of it. The fact changes — name the filters, not the data — and the next action widens the set instead of creating something. The clear-filters control is a quiet secondary action, not a primary CTA.

Filtered-empty
Vendor attestations · 3 filters active

No results match these filters

Try removing a filter or two to widen the set.

Live: clearing the filters brings the rows right back; the region announces the change politely. The slot never scolds anyone for the filters they picked.

Empty is not an error

An empty state is a fact about the data — there is none. An error state is a fact about the attempt — the system could not find out. If the query failed, you do not know the slot is empty; rendering the empty state over a failed fetch tells the user a falsehood. Route failures to /error-states instead.

Empty vs error
Empty — the data is absent

No findings recorded

Run a check to populate this view.

Error — the answer is unknown
Section error

Couldn't load findings

Try again. If it keeps failing, the rest of the page is still working — move on and we'll log this on our side.

Same slot, two different facts. The empty card invites the first action; the error card names the failure and offers a retry. Loading is a third, earlier fact — see the loading pattern.

Variations

First-run empty
Nothing created yet. Primary action: create or attach the first item.
Filtered-empty
Results exist outside the active filters. Secondary action: clear filters.
Permission-empty
The feature is gated and the user has not unlocked it. Secondary action: learn more. No live example yet — it will be authored when a gated surface ships.

Copy formula

Every empty state derives from the same clear-is-kind formula: a noun phrase naming what's empty, then an imperative naming the next action.

text
<Noun phrase naming what's empty>. <Imperative naming the next action>.
  • Do"No evidence attached. Upload a file or link a system to begin."
  • Don't"Sorry, you haven't attached any evidence yet." — apology plus blame; neither helps the reader act.
  • Don't"There's nothing here right now." — names nothing and points nowhere.

Accessibility

  • Heading order. The slot heading is a real heading element at the level the page structure expects — assistive tech finds the empty slot the same way sighted users do.
  • Decorative icon. The icon is aria-hidden; it repeats the heading, it never carries it. Severity glyph pairing (ADR-0017) applies on the error side of the boundary — an empty state has no severity.
  • One real action. The CTA is a canonical Button: keyboard-focusable with a visible focus ring. Don't make the whole slot clickable.
  • Announce filter changes. When clearing filters swaps the slot for rows, the region is aria-live="polite" — not role="alert"; nothing went wrong.
  • Reduced motion. The slot's entrance animation is skipped under prefers-reduced-motion.

Usage

The pattern is a composition, not a component — there is no EmptyState export, and that is deliberate: the slot belongs to whoever owns the data. Compose Card, Text, and Button in place.

First-run empty inside a Cardtsx
import { Inbox, Upload } from 'lucide-react'
import { Button, Card, Text } from '@halo-compliance/ui'

<Card variant="secondary">
  <Card.Body>
    <div className="evidence-empty-slot">
      <span className="evidence-empty-icon" aria-hidden="true">
        <Inbox strokeWidth={1.75} />
      </span>
      <Text variant="h4" as="h3">No evidence attached</Text>
      <Text variant="body-sm" as="p">
        Upload a file or link a system to begin.
      </Text>
      <Button
        variant="primary"
        icon={<Upload strokeWidth={1.75} aria-hidden="true" />}
      >
        Upload evidence
      </Button>
    </div>
  </Card.Body>
</Card>

Anti-patterns

  • Don't apologize or blame. "Sorry" and "you haven't" both spend the reader's attention on feelings instead of facts. State what is, then what to do.
  • Don't show it while loading. An empty state asserts the data is absent. Until the query resolves, that's unknown — show the loading state, then decide.
  • Don't stack actions. One next action. A slot with five buttons is a navigation problem wearing an empty state's clothes.
  • Don't throw confetti. Fun lives in the surfaces, not in overclaiming. An empty review queue is a quiet win — "You're all caught up. No findings need review." says it warmly without the fireworks.
  • /error-states — the other side of the boundary: the attempt failed.
  • /loading — what to show before you know whether the slot is empty.
  • /card — the container that owns most empty slots.
  • /button — the one action the slot offers.