No evidence attached
Upload a file or link a system to get started.
Patterns
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.
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.
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.
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.
Upload a file or link a system to get started.
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.
Try removing a filter or two to widen the set.
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.
Run a check to populate this view.
Try again. If it keeps failing, the rest of the page is still working — move on and we'll log this on our side.
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.
<Noun phrase naming what's empty>. <Imperative naming the next action>.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.
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>