Skip to main content
ENKO

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:

TypeWhen it appearsTone
First-useUser has no data yet (new account, no transactions)Encouraging, onboarding
No resultsSearch or filter returned nothingHelpful, suggestive
ErrorData failed to loadReassuring, actionable
ClearedAll 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:

  1. Illustration or icon — A visual that matches the context (not generic)
  2. Heading — Short, clear statement of the situation
  3. Description — One or two sentences explaining why and what to do
  4. 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.

First-use empty state

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 results
xyznoname

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.

Error state

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.

Cleared state

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

ComponentElementdata-slotNotes
EmptyStatedivempty-stateRoot wrapper
EmptyStateIcondivempty-state-iconaria-hidden="true", rounded background
EmptyStateTitleh3empty-state-titleHeading text
EmptyStateDescriptionpempty-state-descriptionSupporting text
EmptyStateActiondivempty-state-actionCTA container

Sizes

The EmptyState root accepts a size prop: "default" or "sm". Size cascades to all children via group-data-[size=sm]/empty-state.

DefaultCompact (sm)
Padding64px / 32px24px / 16px
Icon container48px40px
Icon size24px20px
Title16px medium14px medium
Description14px regular12px 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

DoDon't
Show a skeleton while loadingShow "No transactions yet" during a slow request
Distinguish error from emptyRender an empty account when the request failed
Say what is true of the balance in an errorSay "Something went wrong" and stop
Show cached data with a timestamp when offlineShow an empty screen because the network is down
Name the search term that found nothingShow a bare "No results"
Offer "Clear filters" when filters are activeLeave 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.