Empty and loading states
Skill param087/saas-ui-skills/skills/empty-and-loading-states
Production-grade SaaS UI skills for AI coding agents (React + Tailwind + shadcn/ui) — Codex, Claude Code, Cursor, OpenCode. One npx command to install.
npx -y skills add param087/saas-ui-skills --skill empty-and-loading-statesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when handling the non-happy UI states — skeleton loaders, empty states with a clear CTA, error states with retry, and optimistic updates. Covers the states agents forget that make an app feel broken or polished.
SKILL.md
3.8 KB, as published. Nobody here has run it
Empty & Loading States
Overview
The difference between a prototype and a product is the non-happy states. Every data view has at least four: loading, empty, error, and success. Agents ship the success state and leave the rest blank — which reads as broken. Design all four, every time.
When to use
- Any component that fetches data.
- A list/table/dashboard that can be empty.
- Mutations that should feel instant (optimistic UI).
The four states
if (isLoading) return <ListSkeleton />;
if (error) return <ErrorState onRetry={refetch} />;
if (data.length === 0) return <EmptyState />;
return <List items={data} />;
Skeletons that match the layout
Mirror the real content's shape so the page doesn't jump when data arrives:
function ListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-live="polite">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 rounded-lg border p-4">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-3 w-1/2" />
</div>
</div>
))}
</div>
);
}
Prefer skeletons over spinners for content regions — they reduce perceived wait and layout shift. A spinner is fine for buttons and small inline actions.
Empty state = an opportunity
A blank screen should orient and prompt action — icon, one-line explanation, and a primary CTA:
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
<Inbox className="h-10 w-10 text-muted-foreground" aria-hidden />
<h3 className="mt-4 text-sm font-semibold">No invoices yet</h3>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
Create your first invoice and it’ll show up here.
</p>
<Button className="mt-4"><Plus className="mr-2 h-4 w-4" /> New invoice</Button>
</div>
);
}
Distinguish first-use empty ("Create your first…") from no-results empty ("No matches for 'foo' — clear filters").
Error state with retry
<div className="flex flex-col items-center gap-3 rounded-lg border border-destructive/30 p-8 text-center">
<AlertCircle className="h-8 w-8 text-destructive" aria-hidden />
<p className="text-sm">Couldn’t load invoices.</p>
<Button variant="outline" size="sm" onClick={onRetry}>Try again</Button>
</div>
Optimistic updates
Apply the change immediately, roll back on failure so the UI feels instant:
async function toggleStar(id: string) {
setItems((prev) => prev.map((i) => i.id === id ? { ...i, starred: !i.starred } : i)); // optimistic
try { await api.toggleStar(id); }
catch { setItems(prev); toast.error("Couldn’t update — reverted"); } // rollback
}
Pitfalls
- Only the success state — blank loading/empty/error reads as broken.
- Full-page spinner for partial loads — skeleton the region instead.
- Layout shift when data replaces the loader — match skeleton dimensions.
- Generic empty state — first-use vs no-results need different copy/CTAs.
- Error with no retry — dead end; always offer a way forward.
- Optimistic update with no rollback — UI lies when the request fails.
Hand-off
The polish layer for data-tables, dashboard-layout, and any fetched view; failures surface through notifications-and-toasts.