Empty states
Guide users when there's no content to display. Empty states turn dead ends into on-ramps — they explain what happened, set expectations, and suggest a next action.
An empty screen in a money app raises a specific fear: is my data gone? The difference between "you have no transfers yet" and "we couldn't load your transfers" is the difference between an onboarding moment and a panic, and the two must never look alike.
Types
Senco uses four categories of empty states, each with a different tone and purpose:
| Type | When it appears | Tone |
|---|---|---|
| First-use | User has no data yet (new account, no transactions) | Encouraging, onboarding |
| No results | Search or filter returned nothing | Helpful, suggestive |
| Error | Data failed to load | Reassuring, actionable |
| Cleared | All items are resolved (no pending tasks) | Positive, celebratory |
Never render the wrong one
The four states are visually similar and semantically opposite. Two rules prevent the worst confusion:
- Never show first-use while loading. A slow request that renders "No transactions yet" tells a user with 200 transfers that their history is gone. Loading shows a skeleton; empty renders only after a confirmed empty response.
- Never show first-use on an error. A failed request is an error state with a retry, not an empty account.
This is the single most important rule on this page, and it is a timing bug rather than a design one — which is why it is documented here, where the design is specified.
Structure
Every empty state follows the same anatomy:
- Illustration or icon — A visual that matches the context (not generic)
- Heading — Short, clear statement of the situation
- Description — One or two sentences explaining why and what to do
- CTA button — A primary action that resolves the empty state (optional for "cleared" type)
The icon is not an image
Empty-state visuals are icons rendered inline, not loaded assets. An empty state that appears because the network failed cannot depend on the network to render its own illustration — see Design principles.
First-use empty state
The most important empty state. New Senco users see this on their transaction history before making their first transfer. The message should feel welcoming and guide them toward their first action.
- Icon: a friendly illustration related to sending money
- Heading: "No transactions yet"
- Body: "Once you send or receive a transfer, it will appear here."
- CTA: "Send your first transfer"
This is the highest-leverage screen in the product for a new user. It is the only moment where the empty state is the onboarding, and its CTA is the primary action of the entire app.
No transactions yet
Once you send or receive a transfer, it will appear here.
No results
Appears when a search or filter returns zero matches. Help the user adjust rather than leaving them stuck.
- Icon: search-related visual
- Heading: "No transfers found"
- Body: "Try adjusting your search terms or clearing filters."
- No CTA button needed — the search input itself is the action area
Say what was searched, and offer the undo
"No transfers found for maria" is more useful than "No transfers found", because it shows the user what the system actually looked for — which is how a typo gets spotted. Where filters are active, the state names them and offers to clear them: "No transfers found · 2 filters active · Clear filters".
The clear-filters action is the CTA in all but name, and without it a user with three filters applied has to remember what they set.
No transfers found
Try adjusting your search terms or clearing your filters.
Error state
When data fails to load, be honest and offer a recovery path. Never blame the user.
- Icon: warning or alert visual
- Heading: "Something went wrong"
- Body: "We couldn't load your transfers. Check your connection and try again."
- CTA: "Try again"
Say what is true of the money
An error state in a transfer app carries a second obligation beyond explaining the failure: it must say what is true of the user's money. "We couldn't load your transfers — nothing has changed and your balance is unaffected" closes the question the user is actually asking.
Offline is its own case
Offline is not a generic error. It has a known cause, a known remedy, and usually cached data to show:
You're offline — showing your transfers as of 3 minutes ago. New transfers will appear when you reconnect.
Cached content with a visible timestamp beats an empty error screen every time, and it is the difference between an app that degrades and an app that stops.
Something went wrong
We couldn't load your transfers. Check your connection and try again.
Cleared state
A positive empty state — the user has no pending actions. This should feel like a small reward.
- Icon: checkmark or celebration visual
- Heading: "All caught up!"
- Body: "You have no pending transfers. Nice work."
- No CTA needed
Cleared is the one empty state allowed a light tone. Even here it stays level — this is a product where the adjacent screen may be a failed payment, and celebration that overreaches reads as tone-deaf. See Brand.
All caught up!
You have no pending transfers. Nice work.
Component API
EmptyState is a compound component with five sub-components. Import from @/components/ui/empty-state.
Sub-components
| Component | Element | data-slot | Notes |
|---|---|---|---|
EmptyState | div | empty-state | Root wrapper |
EmptyStateIcon | div | empty-state-icon | aria-hidden="true", rounded background |
EmptyStateTitle | h3 | empty-state-title | Heading text |
EmptyStateDescription | p | empty-state-description | Supporting text |
EmptyStateAction | div | empty-state-action | CTA container |
Sizes
The EmptyState root accepts a size prop: "default" or "sm". Size cascades to all children via group-data-[size=sm]/empty-state.
| Default | Compact (sm) | |
|---|---|---|
| Padding | 64px / 32px | 24px / 16px |
| Icon container | 48px | 40px |
| Icon size | 24px | 20px |
| Title | 16px medium | 14px medium |
| Description | 14px regular | 12px regular |
Usage
<EmptyState>
<EmptyStateIcon><Send /></EmptyStateIcon>
<EmptyStateTitle>No transactions yet</EmptyStateTitle>
<EmptyStateDescription>
Once you send or receive a transfer, it will appear here.
</EmptyStateDescription>
<EmptyStateAction>
<Button size="sm">Send your first transfer</Button>
</EmptyStateAction>
</EmptyState>
Choosing a size
Default fills a page region — a whole transaction history, a whole tab panel. Compact fits inside a card or a dropdown, such as a recipient search with no matches. A default-size empty state inside a 200px popover pushes everything else off screen.
Design and accessibility
- Icons use
aria-hidden="true"since the heading and description carry the meaning - CTA buttons are focusable and follow the standard Button component API
- Text is concise — no more than two lines of description
- Empty state containers are centered within their parent, with consistent vertical spacing
- All spacing uses DS tokens, no hardcoded values
Announcing an empty result
When an empty state replaces content in response to a user action — a search that returned nothing, a filter that excluded everything — the result is announced politely: "No transfers found". Without it, a screen reader user who searches hears silence and cannot tell whether the search ran.
The heading level follows the page hierarchy rather than being chosen for size. EmptyStateTitle renders an <h3>, which is correct inside a section with an <h2> and wrong directly under an <h1>.
Do and don't
| Do | Don't |
|---|---|
| Show a skeleton while loading | Show "No transactions yet" during a slow request |
| Distinguish error from empty | Render an empty account when the request failed |
| Say what is true of the balance in an error | Say "Something went wrong" and stop |
| Show cached data with a timestamp when offline | Show an empty screen because the network is down |
| Name the search term that found nothing | Show a bare "No results" |
| Offer "Clear filters" when filters are active | Leave the user to remember what they filtered by |
Known gaps
- Loading, empty, and error are specified as distinct states but the guard against rendering empty during loading is a convention, not an enforced component API.
- No offline variant exists in the component; the copy is specified here only.
- Illustrations are described as context-specific but no icon set is defined per state.