Loading & Fallback States

What users see while waiting, when only part of the page loads, or when the app can't recover gracefully.

Page skeleton — dashboard

The user navigated to a page and data is loading. Instead of a spinner or blank screen, show a skeleton that matches the layout they're about to see. This reduces perceived load time and prevents layout shift.

Show immediately on navigation before data arrives. Match the skeleton shape to the actual content layout. Never show a centered spinner for a full page — it gives no information about what's coming.

Table loading skeleton

A data table is loading its rows. The header is already visible (it's static) but the body is still fetching. This is better than hiding the entire table because the user can already see the column structure.

NameEmailRoleStatus

Show inside the table body while rows are loading. Keep the table header visible and static. This works well for paginated tables where the header doesn't change.

Button loading states

The user clicked a button and the action is processing. The button should show a loading state to prevent double-clicks and give feedback. This is one of the most common and most overlooked patterns.

Show on any button that triggers an async action (save, submit, delete, export). Disable the button and show a spinner. Keep the button the same width to prevent layout shift.

Partial page failure

The page loaded but one section failed — maybe the analytics widget timed out while the rest of the dashboard is fine. You shouldn't blow up the entire page for one failed widget.

Active users

2,847

+12% from last week

Failed to load revenue data

This section couldn't connect to the analytics service.

Recent signups

184

+8% from last week

Show inside the specific section that failed, while keeping the rest of the page functional. Always offer a retry for just that section. This is critical for dashboards with multiple independent data sources.

Stale data indicator

The data on the page is cached or outdated. A background refresh failed silently, or the user has been on the page for a long time. You're showing data, but it might not be current.

System status

Last updated 23 min ago

Data may be outdated. Auto-refresh failed — click refresh to update.

API Latency

142ms

Uptime

99.97%

Error Rate

0.02%

Show a subtle banner or timestamp when data is older than expected. Don't hide the data — stale data is better than no data. But be transparent about freshness.

Optimistic update with undo

The user performed an action and you updated the UI immediately (optimistic update), but the server hasn't confirmed yet. If it fails, you need to roll back. Meanwhile, offer an undo window.

Project Alpha

Active

Project Beta

Archived

Project Gamma

Active

Project archived

Show a toast or inline banner immediately after an optimistic action. Give the user a brief window to undo before the action is committed. This is common for delete, archive, and status change actions.

Long-running operation

The user triggered something that takes more than a few seconds — a data export, bulk import, or report generation. A spinner isn't enough. They need to know progress and whether they can leave the page.

Generating report...

Processing 12,847 records

Progress67%

You can leave this page — we'll notify you when it's ready.

Show for any operation that takes more than 3-5 seconds. Include a progress indicator if possible, and always tell the user whether they can navigate away safely.