FSAI Design System
Patterns

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 Skeleton when the final layout is known
  • use Spinner when 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

PatternUse whenTypical primitive
Layout-matching placeholderThe final shape is already knownSkeleton
Small local wait stateA subregion or widget is busySpinner
Button or action in progressThe user triggered a single actionButton isLoading or a small Spinner
Media or heavy preview processingThe surface exists but content is still being preparedSkeleton 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 isLoading behavior
  • 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:

  • Skeleton for 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

PatternPathNotes
Full page skeletonfsai/apps/brand-dashboard/src/pages/Projects/ProjectDetail/ProjectDetail.tsxGood example of preserving page structure.
Progressive table loadingfsai/apps/brand-dashboard/src/pages/Marketing/Finder/FinderResultsTable.tsxSkeleton rows inside a real result table.
Card and carousel skeletonsfsai/apps/brand-dashboard/src/pages/Helpdesk/components/FeaturedCarousel.tsxCard-shaped placeholders that match final layout.
Full panel spinnerfsai/apps/brand-dashboard/src/modules/tasks/components/TaskPanel/TaskPanelDetails/TaskPanelDetails.tsxLocal blocking wait where the layout is not yet worth rendering.
Inline loading-more spinnerfsai/apps/brand-dashboard/src/modules/locations/components/DiscoverLocationsModal/DiscoverLocationsModal.tsxSmall status indicator in a list flow.
Small control placeholderfsai/apps/brand-dashboard/src/pages/Home/CalendarSidebar/CalendarSidebar.components.tsxText-sized skeletons inside already established UI.

Guidelines

  • Do prefer Skeleton when the user can already understand the final layout
  • Do use Spinner for short, local, or action-level waits
  • Do keep loading states proportional to the surface that is busy
  • Do use built-in isLoading props 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

On this page