Skeleton placeholders preserve layout while data loads. Spinners signal in-flight actions. Use both — skeletons for content areas, spinners for buttons and inline fetches.
Skeleton vs spinner
State
Component
Example
Page section loading
Skeleton shape props
Dashboard cards, table rows, profile header
Button action in progress
Button `loading` prop
Save, submit, delete
Full-page blocking load
Centered spinner in Column
Initial route fetch
Background refresh
None — keep stale content visible
Settings panel re-fetch
Skeletons hold space so the layout does not jump when real content arrives. The loading prop on buttons signals activity on an element the user just interacted with.
Skeleton shapes
The skeleton component has three shapes:
Shape
Use for
`line` (default)
Text lines, table rows
`circle`
Avatars, icon placeholders
`block`
Images, chart areas, media cards
Build a profile placeholder with a circle on top and two lines below. Use t-shirt width and height sizes (xs through xl), not pixel values.A media card placeholder uses shape="block" with width="fill" and height="l".
Stagger with delay
Offset shimmer with the delay prop ("1" through "6") when rendering a list of placeholders.Map over four items and pass delay={String(index + 1)} on each row's skeleton children. Match the skeleton layout to the real content it replaces — same gaps, same approximate widths.
Table skeleton
Mirror your table structure with skeleton rows inside the same static panel:
Wrap in a Column surface panel (padding="24", background="surface", border="neutral-alpha-weak", radius="l")
One line skeleton for the section title
Five line skeletons with width="fill" and staggered delay
Swap the skeleton block for your table component once rows are ready.
Button loading state
Pass loading={isSaving} to Button — it swaps the label for a built-in spinner automatically.Do not nest a manual spinner inside a button unless you need a custom layout.
Write this / not this
✅ Once UI way
❌ Common mistake
Skeleton matching final layout
Generic gray div with inline styles
`Button loading={true}`
Disabled button with no feedback
Keep stale content during refresh
Full-page spinner on every re-fetch
`delay` stagger on lists
All skeletons animating in sync
Check yourself
Skeleton shape matches what it replaces (circle for avatar, block for image)
Layout gaps are identical between skeleton and loaded state
Buttons use the loading prop, not a separate overlay
Empty state shown after load completes with zero results