Loading States
Platform guidance for skeletons, spinners, progressive loading, and action-level waiting states.
Overview
Loading states should preserve context and reduce visual jump wherever possible.
The default rule is:
- prefer
Skeletonwhen the final layout is known - use
Spinnerwhen the wait is short, local, transactional, or the layout is not worth faking
This page exists because loading behavior is used throughout the platform but the rules are otherwise scattered across component docs.
Choose The Right Pattern
| Pattern | Use when | Typical primitive |
|---|---|---|
| Layout-matching placeholder | The final shape is already known | Skeleton |
| Small local wait state | A subregion or widget is busy | Spinner |
| Button or action in progress | The user triggered a single action | Button isLoading or a small Spinner |
| Media or heavy preview processing | The surface exists but content is still being prepared | Skeleton plus spinner treatment when needed |
Prefer Skeleton When The Layout Is Known
Use skeletons for:
- pages with stable header/body regions
- table rows and cards
- loaded panels where only the data is missing
- inline text and stat placeholders
<div className="h-full flex flex-col">
<div className="px-6 py-4 border-b border-default">
<Skeleton className="h-8 w-48" />
</div>
<div className="flex-1 p-6">
<Skeleton className="h-64 w-full" />
</div>
</div>This is the pattern used in ProjectDetail.tsx.
Use Spinner When The Wait Is Local Or Indeterminate
Use a spinner when:
- the surface is already established
- the loading region is small
- the wait is short or action-driven
- faking the full layout would add noise instead of clarity
<div className="flex items-center justify-center h-48">
<Spinner className="w-8 h-8" />
</div>This is the pattern used in TaskPanelDetails.tsx.
Progressive Loading
Do not assume every loading state is full-screen or full-panel.
Good progressive patterns include:
- adding skeleton rows to a table while fetching more results
- overlaying a small skeleton onto an existing card or grid item
- showing a tiny spinner near “loading more” text
<div className="flex items-center gap-2 text-faint">
<Spinner className="w-4 h-4" />
<span>Loading more locations...</span>
</div>Button And Action Loading
For submits and button-triggered actions:
- prefer the component’s built-in
isLoadingbehavior - keep the layout stable
- avoid replacing the entire surrounding form with a separate loading state
If the action happens inside an already rendered surface, a small inline spinner is usually better than a skeleton.
Media And File Processing
Some media-heavy experiences combine approaches:
Skeletonfor the known content surface- spinner treatment for decode, processing, or preview generation
This is common in video, PDF, and analytics-report flows.
Brand Dashboard Usage
| Pattern | Path | Notes |
|---|---|---|
| Full page skeleton | fsai/apps/brand-dashboard/src/pages/Projects/ProjectDetail/ProjectDetail.tsx | Good example of preserving page structure. |
| Progressive table loading | fsai/apps/brand-dashboard/src/pages/Marketing/Finder/FinderResultsTable.tsx | Skeleton rows inside a real result table. |
| Card and carousel skeletons | fsai/apps/brand-dashboard/src/pages/Helpdesk/components/FeaturedCarousel.tsx | Card-shaped placeholders that match final layout. |
| Full panel spinner | fsai/apps/brand-dashboard/src/modules/tasks/components/TaskPanel/TaskPanelDetails/TaskPanelDetails.tsx | Local blocking wait where the layout is not yet worth rendering. |
| Inline loading-more spinner | fsai/apps/brand-dashboard/src/modules/locations/components/DiscoverLocationsModal/DiscoverLocationsModal.tsx | Small status indicator in a list flow. |
| Small control placeholder | fsai/apps/brand-dashboard/src/pages/Home/CalendarSidebar/CalendarSidebar.components.tsx | Text-sized skeletons inside already established UI. |
Guidelines
- Do prefer
Skeletonwhen the user can already understand the final layout - Do use
Spinnerfor short, local, or action-level waits - Do keep loading states proportional to the surface that is busy
- Do use built-in
isLoadingprops on shared components where available - Don't default to a centered spinner for every page load
- Don't render a heavy skeleton when a tiny local spinner communicates the state more clearly