| Same visual for true-empty and failed-fetch | Four distinct states; error always says "error" + Retry |
| Blank white screen while loading | Skeleton or spinner per the threshold table |
| Spinner for a full content page | Skeleton screen matching layout |
| Skeleton geometry ≠ final layout (content jumps) | Match dimensions exactly; reserve image space |
| Indicator flashes on an 80ms load | 150–300ms show-delay + 300–500ms minimum display |
| Full-screen blocking spinner for one widget | Localize loading; keep the rest interactive |
| "No data" with no action | Every empty state gets a next step |
| First-use empty that only describes the feature | Lead with the CTA that creates the first item |
| Beginner onboarding copy on a filtered no-results view | Distinct no-results state that echoes the query |
| Optimistic update reverting silently | Revert + visible "failed · Retry" message |
| Optimistic UI on payments/destructive actions | Pessimistic confirm, or undo window |
| Infinite indeterminate spinner on long jobs | Determinate progress, steps, cancel, background notice |
| Shimmer ignoring reduced-motion | Static placeholder under prefers-reduced-motion |
| Refresh that blanks existing content | Keep content visible; swap when fresh data arrives |
| Per-element desynchronized shimmer | One synchronized shimmer across the skeleton group |
One items.length === 0 branch for all four states | Distinct loading/error/first-use/cleared/no-results detection |
| Empty state hidden behind the fold on mobile | Center in the visible content region; drop the illustration if short |