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.
| Name | Role | Status | |
|---|---|---|---|
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
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
ActiveProject Beta
ArchivedProject Gamma
ActiveProject 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
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.