Empty and error states tell users what happened and what to do next. Compose them with Column, Icon, Heading, Text, and Button — not generic placeholder divs.
What this pattern covers
Every data-driven UI needs three non-happy paths:
Empty — query succeeded but returned zero results
Error — fetch failed or action was rejected
No access — user lacks permission to view content
These are different from loading states (skeletons and spinners). Show empty/error only after the request resolves.
Empty state anatomy
A good empty state has four parts:
Icon or illustration (muted, not decorative)
Heading — what is empty
Text — why it might be empty or what to do
Primary action Button (optional secondary)
<Column fillWidth center padding="48" gap="16" maxWidth="xs">
<Icon name="inbox" size="l" onBackground="neutral-weak" />
<Heading as="h3" variant="heading-strong-s" align="center">
No projects yet
</Heading>
<Text variant="body-default-s" onBackground="neutral-weak" align="center">
Create your first project to start tracking work.
</Text>
<Button arrowIcon>Create project</Button>
</Column>
Center the block inside its parent with center on Column. Use maxWidth="xs" so text lines stay readable.
Inside a table or list panel
When a table has no rows, replace the Table with an empty state inside the same surface panel:
Use onBackground="danger-medium" on the Icon only — not on the entire panel. Reserve danger borders for destructive actions (see settings danger zones).
Inline error vs full-page error
Scope
When
Layout
Inline
Single widget failed (chart, sidebar widget)
Compact Column inside the widget's bounds
Section
Main content area failed
Centered empty state in content Column
Full page
Route-level failure
Column with `minHeight` filling the viewport
For inline errors, skip the Icon and use Text with a retry Button:
Filtered empty vs truly empty
Distinguish "no data exists" from "no matches for your filters":
Situation
Heading
Action
No data yet
"No projects yet"
Create / import Button
Filters returned zero
"No matching projects"
Clear filters Button (secondary)
Search returned zero
"No results for '{query}'"
Adjust search Text, no Button needed
Toast vs inline error
Use inline/section error states for persistent failures the user needs to act on. Use toast feedback for transient action failures (save failed, copy failed) where the form context is still visible.
Failure type
Surface
Page/section fetch failed
Inline or section error state
Form submit rejected
Toast + keep form visible with field errors
Background sync failed
Toast notification
Agent checklist
When generating list or dashboard views:
Handle three render branches: loading → empty/error → data
Never show skeleton and empty state simultaneously
Include a primary action in empty states when the user can create data
Include retry in error states when the operation can be re-attempted
Differentiate filtered-empty from truly-empty copy
Write this / not this
✅ Once UI way
❌ Common mistake
Icon + Heading + Text + Button stack
Plain "No data" string centered in a div
Empty state inside existing panel
Remove panel chrome when empty
Retry Button on errors
Error with no recovery path
"Clear filters" for filtered empty
Same copy as truly-empty state
Toast for transient action failures
Full-page error for a failed save
Check yourself
Loading skeleton removed before showing empty/error
Empty state has heading, explanation, and action when applicable
Error state has retry or support path
Filtered-empty uses different copy than truly-empty
Danger colors limited to Icon accent, not full panel background