figma guide

Designing loading states and skeleton screens in Figma: patterns, timing, and handoff

Design skeleton loaders and loading states in Figma with shimmer variants, content placeholders, spinners, and Dev Mode specs so engineers ship the right wait experience.

Published
Updated
Jun 09, 2026
Read time
7 min
Level
Beginner

Quick answer

Loading states in Figma are placeholder UI shown while data or assets are still fetching. Build skeleton screens that mirror the final layout (avatar circle + text lines + button blocks) instead of a lone spinner whenever content shape is predictable. Publish components with variants for type=skeleton | spinner | progress, density=compact | default, and optional shimmer overlay. Document minimum display time (300ms to avoid flash), transition to content (fade or crossfade), and when to show empty vs error after load completes. Pair with empty states, tables, and cards. Start from the Figma guides hub if you are new to component libraries.


Who this is for

  • Product designers shipping dashboards, lists, and detail pages where data arrives asynchronously.
  • Design system teams who need one skeleton primitive per content block—not a different gray box per squad.
  • Engineers implementing React Suspense, TanStack Query, or custom fetch hooks who need layout-stable placeholders.

Loading pattern picker: skeleton vs spinner vs progress

PatternWhen to useUser expectationAvoid when
Skeleton screenLayout is known (list, card, profile)“Content is on the way”Load time is under 300ms
Spinner (indeterminate)Unknown layout or tiny inline wait”Working…”Full-page loads over 2s
Progress barDeterminate work (upload, import)“X% complete”Indeterminate API calls
Inline skeletonSingle row or field refreshingPartial updateEntire page is blank

Verdict: default to skeleton for page-level and list-level loads. Reserve spinners for buttons, modals, and inline actions. Use progress only when you can report real percentage.


Component anatomy: skeleton block

PartPurposeSpec tip
ContainerMatches final layout boundsSame width/height as loaded content
Placeholder shapesMimic text, media, controlsRounded rects; circle for avatars
Shimmer overlay (optional)Motion cueGradient sweep; note “CSS animation” in Dev Mode
BackgroundPage or card surfaceUse surface/skeleton token
Skeleton / Card
├── Variant: type=card | list-row | table-row | profile | text-block
├── Variant: density=compact | default
├── Property: hasShimmer=true | false
├── Layer: Container (auto layout matches real card)
│   ├── Media block (rect 16:9 or 1:1)
│   ├── Title line (h-12px, w-60%)
│   ├── Body line 1 (h-8px, w-90%)
│   ├── Body line 2 (h-8px, w-75%)
│   └── Action block (h-32px, w-96px)

Bind fills to semantic tokens—typically fill/skeleton at 8–12% opacity on the surface. Preview in dark mode so skeletons do not glow too bright.


Skeleton variants by content type

Content typeSkeleton structureNotes
List rowCircle 40px + 2 text linesMatch navigation item height
Card gridImage rect + title + 2 lines + buttonMirror card anatomy
TableHeader row static; 5–8 skeleton rowsLink to table patterns
Profile headerLarge circle + 2 lines + stat chipsUse badge placeholders for chips
FormLabel line + field rect per inputPair with form field specs
Dashboard widgetChart rect + legend linesKeep chart area aspect ratio

Design one skeleton per real component—swap the loaded instance for the skeleton instance in prototypes to validate layout shift.


Spinner and button loading states

ContextPatternSpec
Primary buttonSpinner replaces label or sits left of labelDisable button; document aria-busy
Icon buttonSpinner centered in 40×40 hit areaMaintain button dimensions
Modal submitSpinner in button + dimmed formLink to modal patterns
Full-pageCentered spinner 32–48pxUse only when layout unknown
Inline fieldSmall spinner 16px right of inputPair with validation states

Publish Button / Loading as a variant of your button component with state=loading—not a separate one-off frame per screen.


Shimmer and motion notes

Figma prototypes cannot export production shimmer CSS, but you can communicate intent:

  1. Add a diagonal gradient rectangle over skeleton shapes (low opacity).
  2. In Dev Mode description: “Shimmer: 1.5s linear infinite; translate gradient -100% → 100%.”
  3. Note reduced motion: when prefers-reduced-motion, show static skeleton without animation.

For prototyping demos, a simple opacity pulse on skeleton blocks is enough to review with stakeholders—label it “stand-in for engineering shimmer.”


Layout stability: prevent content jump

RuleWhyHow in Figma
Match dimensionsAvoid CLSSkeleton same height as loaded card
Reserve image ratioNo layout shiftFixed aspect-ratio frame
Min height on listsNo collapseContainer min-height = N rows × row height
Sticky chromeNav stays putLoad only content region

Use auto layout on both skeleton and loaded variants so spacing tokens stay identical. Compare side-by-side on one artboard: Loading | Loaded | Empty | Error.


State flow after load completes

Document this sequence for every data view:

1. Initial → skeleton (or spinner if layout unknown)
2. Success + data → populated content
3. Success + zero items → empty state (not skeleton)
4. Failure → error state with retry CTA

Verdict: never show an empty state during fetch—users interpret “no data” as final. Never leave skeleton visible after error.

Show all four outcomes on a state matrix artboard per feature. Cross-link wireframing workflows when exploring states early.


Progressive and staggered loading

StrategyUXFigma deliverable
Shell firstNav + header load; content skeletonSeparate frames: shell / content-loading
Staggered rowsRows appear top-to-bottomNote delay 50–100ms per row in spec
Above-the-fold priorityHero loads before footerTwo skeleton zones with priority labels
Optimistic UIShow stale data + inline refresh”Refreshing…” inline skeleton on row

For tables, show column headers immediately with skeleton body rows—headers should not pulse.


Accessibility checklist

  • aria-busy="true" on loading regions—document in component description.
  • aria-live="polite" when content replaces skeleton—note for screen reader announcement.
  • Do not rely on motion alone—skeleton shape communicates loading without shimmer.
  • Focus management: if a modal opens in loading state, trap focus and announce status.
  • Minimum contrast on skeleton fills—check with accessibility plugins.
  • Reduced motion variant: static skeleton, no pulse.

Handoff to engineering

DeliverableWhere it lives
Skeleton ↔ content mappingComponent variant names
Min display time”Show skeleton ≥ 300ms”
Transition”Crossfade 200ms” or “instant swap”
Row count (table)“Render 8 skeleton rows”
Error / empty triggersState matrix artboard
Shimmer CSS referenceDev Mode description link

Include loading states in your Dev Mode handoff checklist. Publish skeletons in your team library next to the loaded components they mirror.


Common mistakes

MistakeConsequenceFix
Full-page spinner for known layoutLayout jump on loadSkeleton matching final UI
Skeleton after data arrivesFlash of grayMin time + fast fetch handling
Empty state during load”No data” confusionSkeleton until fetch settles
Random gray boxesInconsistent productTokenized skeleton shapes
Skeleton smaller than contentCLSMatch dimensions exactly
Shimmer on error stateFeels brokenSwap to error component
No loading on button submitDouble-submitButton loading variant
Different spacing vs loaded cardVisual snapShared auto layout structure

FAQ

How many skeleton rows should a table show?

5–8 rows is typical—enough to imply a list without implying exact count. Document that engineers should not render 100 skeleton rows.

Should skeletons animate in Figma libraries?

Static is fine for the published component. Add a separate demo frame with prototype pulse if stakeholders need to see motion intent.

One skeleton component for everything?

One base skeleton with type variants (card, row, text-block). Use nested components for repeated line shapes.

What about image loading inside a loaded card?

Use an inline skeleton or blurred placeholder inside the image frame—document loading="lazy" behavior separately from page-level skeleton.


Bottom line

Treat loading states as layout-stable placeholders that mirror real components—not generic gray screens. Default to skeletons for lists, cards, and tables; use spinners for buttons and unknown layouts; use progress only when percentage is real. Document the full load → success → empty → error flow on state matrix artboards and publish skeleton variants beside loaded components in your library. Continue with empty states, tables, and the tutorials hub.

Share on X

§ Keep reading

Related guides.