Empty, loading, and error states
Designing clear, consistent, and production-ready async states with PyColors UI.
Modern products spend a significant amount of time in non-happy states.
Data is loading. Lists are empty. Errors happen.
How you handle these states has a direct impact on:
- perceived performance
- user confidence
- overall product quality
This guide explains how to design empty, loading, and error states using PyColors UI primitives — without introducing product logic into UI components.
The three async states
Every data-driven UI moves through three distinct states:
- Loading → data is being fetched
- Empty → request succeeded, but no data exists
- Error → request failed or action could not be completed
Each state has a different intent and requires a different UI response.
Loading states → Skeleton
When to use Skeleton
Use Skeleton when:
- data is being fetched
- layout is already known
- content will appear soon
Skeleton preserves layout structure and reduces visual jank.
What Skeleton represents
Skeleton is not content. Skeleton is not feedback.
It is a temporary visual placeholder.
Example: Card loading
<Card>
<CardContent className="space-y-3">
<Skeleton className="h-5 w-1/3" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-24 w-full" />
</CardContent>
</Card>Example: Table loading
<Table>
<TableBody>
{Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i}>
<TableCell>
<Skeleton className="h-4 w-32" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-16" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>Empty states → EmptyState
When to use EmptyState
Use EmptyState when:
- the request succeeded
- there is no data to display
- the state is valid and expected
EmptyState answers a simple question:
“Why is this empty — and what can I do next?”
Example: Empty section
No projects yet
Create your first project to get started.
<EmptyState
title="No projects yet"
description="Create your first project to get started."
action={<Button>Create project</Button>}
/>Example: Table empty
No resultsTry adjusting your filters. |
<Table>
<TableBody>
<TableRow>
<TableCell colSpan={4}>
<EmptyState
title="No results"
description="Try adjusting your filters."
/>
</TableCell>
</TableRow>
</TableBody>
</Table>Error states → Alert
When to use Alert
Use Alert when:
- an action fails
- data cannot be loaded
- the user needs to be informed immediately
Alerts communicate problems, not absence.
Example: Inline error
Something went wrong
We couldn’t load your projects. Please try again.
<Alert variant="destructive">
<AlertTitle>Something went wrong</AlertTitle>
<AlertDescription>
We couldn’t load your projects. Please try again.
</AlertDescription>
</Alert>Choosing the right state
A simple decision table:
| Situation | Component |
|---|---|
| Data is loading | Skeleton |
| Request succeeded, no data | EmptyState |
| Action or request failed | Alert |
Never mix these states.
UX rules (production-tested)
- Skeleton never explains — it only preserves layout
- EmptyState always explains why the state exists
- Error states should be clear, calm, and actionable
- Avoid flashing between states
- Prefer consistency over clever animations
- Replace skeletons quickly once data is ready
Anti-patterns to avoid
- Skeleton with explanatory text
- EmptyState used for errors
- Toast used for critical failures
- Loading spinners replacing entire layouts
- Complex logic inside UI primitives
Mental model to keep
Skeleton = shape EmptyState = explanation Alert = problem
Keeping these roles distinct is one of the simplest ways to make a product feel stable and professional.