Empty, loading, and error states
Design clear, consistent, and production-ready async states with PyColors UI primitives.
OverviewLink to section
Modern products spend a large part of their life outside the happy path.
Data loads. Lists are empty. Requests fail. Actions can be delayed, rejected, retried, or blocked by permissions.
How your product handles these moments directly affects:
- perceived performance
- user confidence
- product clarity
- overall product quality
This guide explains how to design loading, empty, and error states with PyColors UI primitives.
Why this matters
Users do not only judge the successful state. They also judge whether the product feels stable when data is missing, slow, or unavailable.
What you’ll designLink to section
Loading states
Preserve the expected layout while data is still loading.
Empty states
Explain valid empty results and guide the user toward the next action.
Error states
Communicate failures calmly, clearly, and with an actionable recovery path.
Mental modelLink to section
Skeleton is shape. EmptyState is explanation. Alert is a problem.
Each async state has one job:
| State | Component | Meaning |
|---|---|---|
| Loading | Skeleton | Data is expected soon |
| Empty | EmptyState | The request worked, but there is nothing to show |
| Error | Alert | Something failed and the user needs feedback |
System rule
Do not blur async states. A product feels clearer when every state communicates one specific situation.
StepsLink to section
Use Skeleton for loadingLink to section
Use Skeleton when data is loading and the final layout is already known.
Skeleton should preserve shape and reduce layout shift. It should not explain the state.
import { Card, CardContent, Skeleton } from "@pycolors/ui";
export function ProjectCardSkeleton() {
return (
<Card className="rounded-2xl border-border/60">
<CardContent className="space-y-3 p-6">
<Skeleton className="h-5 w-1/3" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-24 w-full" />
</CardContent>
</Card>
);
}import {
Skeleton,
Table,
TableBody,
TableCell,
TableRow,
} from "@pycolors/ui";
export function ProjectTableSkeleton() {
return (
<Table>
<TableBody>
{Array.from({ length: 3 }).map((_, index) => (
<TableRow key={index}>
<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>
);
}Use EmptyState for valid empty resultsLink to section
Use EmptyState when the request succeeded but there is nothing to display yet.
An empty state should answer two questions:
- why is this empty?
- what can the user do next?
import { Button, EmptyState } from "@pycolors/ui";
export function ProjectsEmptyState() {
return (
<EmptyState
title="No projects yet"
description="Create your first project to start organizing your workspace."
action={<Button>Create project</Button>}
/>
);
}This is not a failure. It is a real product state.
Use Alert for errorsLink to section
Use Alert when a request or action fails and the user needs to understand what happened.
import {
Alert,
AlertDescription,
AlertTitle,
Button,
} from "@pycolors/ui";
export function ProjectsErrorState() {
return (
<Alert variant="destructive">
<AlertTitle>Projects could not be loaded</AlertTitle>
<AlertDescription>
Check your connection and try again.
</AlertDescription>
<div className="mt-4">
<Button variant="outline">Retry</Button>
</div>
</Alert>
);
}Error copy should be calm, specific, and actionable.
ExamplesLink to section
No projects yet
Create your first project to get started.
Something went wrong
We couldn’t load your projects. Please try again.
Decision guideLink to section
Use Skeleton when:
- data is loading
- the layout is predictable
- content is expected soon
- you want to avoid layout shift
Use EmptyState when:
- the request succeeded
- the result is valid
- there is nothing to display
- the user needs a next step
Use Alert when:
- a request failed
- an action failed
- the user cannot proceed
- the product needs to explain a blocking issue
Prefer
- Skeleton when layout is known
- EmptyState when the result is valid but empty
- Alert when the user must understand a failure
- consistent async behavior across product areas
- clear messaging with one role per state
Avoid
- Skeleton combined with explanatory copy
- EmptyState used for real errors
- global spinners replacing stable layouts
- toasts for blocking page failures
- mixing multiple async states at once
Production checklistLink to section
Before shipping async states, confirm that:
- loading states preserve layout when possible
- empty states explain why nothing is shown
- empty states offer a useful next action when relevant
- error states explain what happened
- blocking failures are visible inline, not only in a toast
- retry actions are available when recovery is possible
- copy stays calm, specific, and human
Ready to use these states in real product flows?
Starter Pro gives you production-shaped SaaS foundations where async states matter: auth, billing, protected routes, settings, and product dashboards. → /docs/starter-pro
Common questionsLink to section
Next stepsLink to section
Use visual placeholders for loading states that preserve layout.
Design valid empty states with clear messaging and next actions.
Communicate blocking problems and important product feedback.
Apply the same clarity to form validation, loading, and submission errors.