Design effective loading states, skeleton screens, and empty states that maintain user confidence. Use when content takes time to load, when showing progress, or handling empty data scenarios...
Make sure no user ever stares at a blank or frozen-looking screen: inventory every place the app waits or can be empty, choose the correct state for each by expected latency, and implement it. The prime directive: the indicator is chosen by how long the wait is and whether the layout is known — not by taste. Deliver an audit table covering every async surface plus the implemented states, never a lone spinner component.
Use for: loading indicators, skeleton screens, progress bars, empty states (first-use, no-results, error-empty, success-empty), "app looks frozen" complaints.
Hand off instead when the real need is:
skills/frontend-design/performance-optimization — always consider that first; the best loading state is noneskills/frontend-design/error-handling-recoveryskills/frontend-design/interaction-physicsfetch(, axios, useQuery/useSWR/createResource, isLoading/isPending/loading flags, Suspense boundaries, and route-level data loaders. List every view/component that waits on data or mutation.Ask the user (one batch, only if not inferable): whether any operations are known to be long-running (uploads, exports, batch jobs), and whether there's a design language for illustrations/empty states. Otherwise state assumptions and proceed — never block an audit on questions the code answers.
| Expected wait | Treatment |
|---|---|
| < 100ms | Nothing — an indicator that flashes in and out is worse than none |
| 100ms – 1s | Subtle inline cue: button spinner/opacity, cursor: wait; no layout change |
| 1s – 10s | Skeleton if the incoming layout is known; spinner if not |
| > 10s | Progress bar — determinate with count/percent if measurable, indeterminate + explanatory text if not |
Fork rules:
Every empty view gets one of four treatments — classify, then apply:
| Type | Trigger | Must include |
|---|---|---|
| First use | User has no data yet | What this area is for + primary CTA to create the first item |
| No results | Search/filter matched nothing | Echo of what was searched + escape hatch ("Clear filters") |
| Error-empty | Load failed | Plain-language failure + Retry action (hand complex recovery to skills/frontend-design/error-handling-recovery) |
| Success-empty | All items done (inbox zero) | Positive confirmation; no CTA needed |
Rule: an empty state without an action is a dead end — only success-empty may omit the CTA.
aria-busy="true" and a polite live region announcing "Loading"; skeletons are aria-hidden="true"; progress bars use role="progressbar" with aria-valuenow/min/max; shimmer stops under prefers-reduced-motion.references/patterns.md — read it when writing the code.Simulate slow network (devtools throttling or an artificial delay) and confirm: no blank frames, no layout shift at content swap, indicators don't flash on fast responses, empty states render with their actions.
Deliver both artifacts:
1. The code — implemented skeleton/spinner/progress/empty-state components and their wiring into each async surface, reusing existing project components and tokens.
2. Loading & Empty State Audit (markdown):
## Async surface inventory
| Surface | Wait class | Current | Correct state | Status |
| Product grid | 1–10s (search API) | blank screen | card skeleton ×8 | implemented |
| Save button | 100ms–1s | none | inline button spinner | implemented |
| CSV export | >10s | spinner | determinate progress + count | implemented |
| Comments | unknown → assume 1s class | layout jump | skeleton, space reserved | implemented |
## Empty state coverage
| View | First use | No results | Error-empty | Success-empty |
| Projects list | ✓ CTA "Create project" | n/a | ✓ Retry | n/a |
| Search page | n/a | ✓ "Clear filters" | ✓ Retry | n/a |
## Assumptions
[Latency estimates you could not verify, flagged for the user]
aria-busy, live-region announcement, role="progressbar" values, aria-hidden skeletons, reduced-motion handling all presentHard don'ts: no spinner-for-everything; no invented time estimates shown to users ("about 2 minutes") unless derived from real data; no loading state on operations that complete instantly.
skills/frontend-design/performance-optimization → run first or alongside; optimistic UI removes the need for many loading states, and skeleton space-reservation is shared CLS work.skills/frontend-design/error-handling-recovery ← consumes your error-empty placements and expands them into full recovery flows.skills/frontend-design/interaction-physics → supplies shimmer/spinner motion timing and the reduced-motion baseline.skills/frontend-design/component-architecture → provides the component inventory your skeletons must mirror; add Skeleton/EmptyState as shared components there.references/patterns.md — full implementation gallery: skeleton building blocks and card example, spinner and button-loading CSS, determinate/indeterminate progress bars, all four empty-state markups, and the accessibility blocks. Read it when writing the code.