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
| Pattern | When to use | User expectation | Avoid when |
|---|---|---|---|
| Skeleton screen | Layout 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 bar | Determinate work (upload, import) | “X% complete” | Indeterminate API calls |
| Inline skeleton | Single row or field refreshing | Partial update | Entire 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
| Part | Purpose | Spec tip |
|---|---|---|
| Container | Matches final layout bounds | Same width/height as loaded content |
| Placeholder shapes | Mimic text, media, controls | Rounded rects; circle for avatars |
| Shimmer overlay (optional) | Motion cue | Gradient sweep; note “CSS animation” in Dev Mode |
| Background | Page or card surface | Use 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 type | Skeleton structure | Notes |
|---|---|---|
| List row | Circle 40px + 2 text lines | Match navigation item height |
| Card grid | Image rect + title + 2 lines + button | Mirror card anatomy |
| Table | Header row static; 5–8 skeleton rows | Link to table patterns |
| Profile header | Large circle + 2 lines + stat chips | Use badge placeholders for chips |
| Form | Label line + field rect per input | Pair with form field specs |
| Dashboard widget | Chart rect + legend lines | Keep 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
| Context | Pattern | Spec |
|---|---|---|
| Primary button | Spinner replaces label or sits left of label | Disable button; document aria-busy |
| Icon button | Spinner centered in 40×40 hit area | Maintain button dimensions |
| Modal submit | Spinner in button + dimmed form | Link to modal patterns |
| Full-page | Centered spinner 32–48px | Use only when layout unknown |
| Inline field | Small spinner 16px right of input | Pair 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:
- Add a diagonal gradient rectangle over skeleton shapes (low opacity).
- In Dev Mode description: “Shimmer: 1.5s linear infinite; translate gradient -100% → 100%.”
- 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
| Rule | Why | How in Figma |
|---|---|---|
| Match dimensions | Avoid CLS | Skeleton same height as loaded card |
| Reserve image ratio | No layout shift | Fixed aspect-ratio frame |
| Min height on lists | No collapse | Container min-height = N rows × row height |
| Sticky chrome | Nav stays put | Load 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
| Strategy | UX | Figma deliverable |
|---|---|---|
| Shell first | Nav + header load; content skeleton | Separate frames: shell / content-loading |
| Staggered rows | Rows appear top-to-bottom | Note delay 50–100ms per row in spec |
| Above-the-fold priority | Hero loads before footer | Two skeleton zones with priority labels |
| Optimistic UI | Show 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
| Deliverable | Where it lives |
|---|---|
| Skeleton ↔ content mapping | Component variant names |
| Min display time | ”Show skeleton ≥ 300ms” |
| Transition | ”Crossfade 200ms” or “instant swap” |
| Row count (table) | “Render 8 skeleton rows” |
| Error / empty triggers | State matrix artboard |
| Shimmer CSS reference | Dev 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
| Mistake | Consequence | Fix |
|---|---|---|
| Full-page spinner for known layout | Layout jump on load | Skeleton matching final UI |
| Skeleton after data arrives | Flash of gray | Min time + fast fetch handling |
| Empty state during load | ”No data” confusion | Skeleton until fetch settles |
| Random gray boxes | Inconsistent product | Tokenized skeleton shapes |
| Skeleton smaller than content | CLS | Match dimensions exactly |
| Shimmer on error state | Feels broken | Swap to error component |
| No loading on button submit | Double-submit | Button loading variant |
| Different spacing vs loaded card | Visual snap | Shared 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.
§ Keep reading