Empty and Error States skill for Claude Code
No blank screens, no endless spinners, no dead ends. Before building the happy path, Claude fills a state matrix for each view covering first use, no results, loading, partial data, errors by HTTP status, offline, forbidden and not found. Each state gets a layout, plain-language copy and a next action, implemented through one consistent pattern per data source and verified by simulating every status with MSW and network throttling.
- name
empty-error-states - size191 lines · ≈2,200 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Designs and implements every non-happy state in an app — first-use and no-results empty states, loading with delay thresholds and skeletons, partial data, inline and page-level errors mapped from HTTP status, offline, permission denied, not found, and success — with a state matrix so none are missed. Use when building a new view or data fetch, when users see blank screens, endless spinners, or "Something went wrong", or when auditing an app for missing states. Try asking
- Our app shows a blank screen when a new user has no projects. Design proper empty states
- Audit this dashboard for missing loading and error states
- Replace our generic 'Something went wrong' errors with messages that help people recover
Use it when
- Building a new view backed by data fetching
- Users report blank screens, flashing spinners, or vague errors
- Auditing an app for missing states before launch
- Designing onboarding for brand-new accounts
Skip it when
- You only need wording for buttons and messages, use ux-microcopy instead
- You need server-side error handling and logging design rather than UI states
How to check the result
Every cell of the state matrix is implemented or marked N/A, and each one is reproduced by throttling, request blocking, MSW status handlers, offline mode and a fresh account.
The common mistake it prevents
Checking items.length === 0 before the request resolves, which flashes the empty state on every single page load.
States are done when every data-driven view has a designed answer for: nothing yet, nothing matches, still loading, partly loaded, failed, offline, and not allowed; each answer tells the user what happened and what to do next; and no state is a blank area, an infinite spinner, or a raw error code.
Ground rules
- Enumerate states before building the happy path. Most production bugs users see are missing states, not wrong happy paths.
- Every non-happy state has a next action: create, clear filters, retry, sign in, request access, go back, contact support.
- Errors are local. A failed widget shows an error in the widget; the page stays usable.
- Preserve user input and context through every error.
- Loading indicators must not flash: nothing under ~300ms, then hold for at least ~500ms once shown.
Workflow
- [ ] 1. Build the state matrix for each view
- [ ] 2. Design each state (layout, copy, action)
- [ ] 3. Implement a single state-switch pattern per data source
- [ ] 4. Map errors (HTTP status / exception type) to user messages
- [ ] 5. Simulate every state and verify
1. State matrix
Copy per view and fill every cell (or mark N/A with a reason):
| State | Trigger | What the user sees | Primary action |
|---|---|---|---|
| Ideal | Data present | — | — |
| Empty: first use | Account has no items ever | Value + how to start | Create first item / import |
| Empty: no results | Search/filter returns 0 | Echo query, suggest fixes | Clear filters |
| Empty: cleared | User completed or deleted all | Positive confirmation | Next logical step |
| Loading: initial | First fetch | Skeleton in final layout | — |
| Loading: refresh | Refetch with data shown | Keep data, subtle indicator | — |
| Loading: slow (> 10s) | Long job | Progress + estimate, allow leaving | Notify me when done |
| Partial | Some sources failed or paginated | Data + inline notice of what is missing | Retry missing part |
| Error: recoverable | 5xx, timeout, network | What failed, data preserved | Retry |
| Error: validation | 400/422 | Inline field errors | Fix field |
| Offline | navigator.onLine === false / fetch fails | Banner, cached data if any | Retry when online (auto) |
| Unauthenticated | 401 | Session expired message | Sign in (return to same page) |
| Forbidden | 403 | Why, who can grant access | Request access |
| Not found | 404 | It was moved/deleted or never existed | Go to list / search |
| Rate limited | 429 | Wait time | Retry after N seconds |
| Too much data | Result exceeds limit | Show first N, explain | Narrow filters / export |
| Success | Mutation completed | Confirmation proportional to the action | Undo / view result |
2. Design each state
Empty state anatomy (first use):
[Optional small illustration or product preview — skip generic clip art]
Headline: what this space is for "Track every invoice in one place"
One sentence: the benefit or how it fills "Invoices you create or import appear here."
Primary action [Create invoice]
Secondary action Import from CSV · Watch 2-min demo
- Prefer showing a preview of what the filled state looks like (sample row, ghosted chart) over a mascot.
- No-results states echo the query and offer specific fixes: “No invoices match ‘acme’ in Paid. [Search all statuses] [Clear filters]”.
- Keep empty states the same height as the populated view to avoid layout jumps.
Error message anatomy: what happened + why (if known) + what to do, in plain language, placed next to the cause.
| Before | After |
|---|---|
| “Error 500” | “We couldn’t load your invoices. This is on our side. [Try again]” |
| “Invalid input” | “Enter a due date after the issue date (Mar 3, 2026).” |
| “Forbidden” | “Only workspace admins can change billing. Ask Dana Kim (admin) for access. [Request access]” |
| “Network error” | “You’re offline. Changes are saved on this device and will sync when you reconnect.” |
Wording details live in ux-microcopy.
Loading thresholds:
| Expected wait | Pattern |
|---|---|
| < 300ms | Nothing; avoid flashing indicators |
| 300ms – 1s | Subtle inline indicator (button spinner, progress bar at top) |
| 1s – 10s | Skeleton matching final layout, or spinner with label for actions |
| > 10s | Determinate progress with estimate; let the user leave and notify on completion |
3. Implement one pattern per data source
React with TanStack Query (v5):
function InvoiceList({ filters }: { filters: Filters }) {
const q = useQuery({ queryKey: ["invoices", filters], queryFn: () => fetchInvoices(filters) });
if (q.isPending) return <InvoiceListSkeleton rows={8} />;
if (q.isError) return <InlineError error={q.error} onRetry={() => q.refetch()} />;
if (q.data.items.length === 0)
return hasActiveFilters(filters) ? <NoResults filters={filters} /> : <FirstUseEmpty />;
return (
<>
{q.isFetching && <TopProgress />} {/* refresh: keep data visible */}
{q.data.partial && <PartialNotice missing={q.data.missing} />}
<InvoiceTable items={q.data.items} />
</>
);
}
- Use an error boundary per region (not one at the app root) so a render crash in one widget does not blank the page.
- With Suspense, place boundaries around independent regions; a single page-level boundary recreates the full-page spinner problem.
- Delay skeleton display (~300ms) for fast endpoints to avoid a flash; many apps do this
with a small
useDelayedFlaghook or a CSSanimation-delayon the skeleton fade-in.
4. Map errors to messages
export function toUserError(err: unknown): { title: string; action: "retry" | "signin" | "request" | "back" | "none" } {
const status = (err as { status?: number })?.status;
if (!navigator.onLine) return { title: "You're offline.", action: "retry" };
switch (status) {
case 401: return { title: "Your session expired. Sign in to continue.", action: "signin" };
case 403: return { title: "You don't have access to this.", action: "request" };
case 404: return { title: "This item was moved or deleted.", action: "back" };
case 409: return { title: "Someone else changed this. Reload to see the latest version.", action: "retry" };
case 429: return { title: "Too many requests. Try again in a minute.", action: "retry" };
default: return { title: "We couldn't complete that. Try again.", action: "retry" };
}
}
Log the technical detail (request ID, status, endpoint) and show the request ID in the UI for support (“Reference: req_8f2a…”) on server errors.
Forms
- Validate on submit first, then on blur/change for fields already shown as invalid.
- Inline error below the field, linked with
aria-describedby; setaria-invalid="true". - On submit failure with several errors, show a summary at the top that links to each field, and move focus to it.
- Never clear the form on error.
Gotchas
items.length === 0before data arrives renders the empty state for a split second on every load; check the pending state first.- A retry button that retries instantly against a down service causes hammering; add backoff or disable for a few seconds.
- Toasts for errors that need action disappear before users read them; keep actionable errors inline and persistent.
- Skeletons that do not match the final layout cause CLS when content arrives.
- Optimistic updates need a rollback state and a visible message when the server rejects.
- “Something went wrong” with no action is a dead end; there is always at least “Try again” or “Contact support” with the reference ID.
- Accessible announcements: wrap async status messages in an
aria-live="polite"region (errors that block progress can userole="alert").
Verification
- Simulate each state: DevTools network throttling (Slow 4G) and request blocking, an
MSW handler per status code (
http.get(url, () => new HttpResponse(null, { status: 403 }))), offline mode, a fresh account, and a filter with no matches. - Storybook (or equivalent) story per state for shared components.
- Every cell in the state matrix is implemented or explicitly N/A.
- Screen reader announces loading completion and errors once, not repeatedly.
Output contract
## States: <view>
| State | Implemented | Component | Copy | Action |
|---|---|---|---|---|
| First use | yes | FirstUseEmpty | "Track every invoice…" | Create invoice |
| No results | yes | NoResults | echoes query | Clear filters |
| Error 5xx | yes | InlineError | "We couldn't load…" + ref ID | Retry |
| 403 | N/A | — | route guarded server-side | — |
Simulated with: MSW handlers in src/mocks/states.ts About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.