GuidesUpdated April 27, 2026

Empty, loading, and error states

Design clear, consistent, and production-ready async states with PyColors UI primitives.

GuidesAsync states

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:

StateComponentMeaning
LoadingSkeletonData is expected soon
EmptyEmptyStateThe request worked, but there is nothing to show
ErrorAlertSomething 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.

components/project-card-skeleton.tsx
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>
  );
}
components/project-table-skeleton.tsx
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?
components/projects-empty-state.tsx
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.

components/projects-error-state.tsx
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