Invited Alisa Hester to the team
Skeletons
How a DrumKit screen is never blank: when a region gets a skeleton rather than a spinner or nothing, the three shapes every loading state is built from, the fifteen family skeletons and what each keeps real, how many to show, when they appear and what replaces them, and the shimmer.
Skeletons
How a DrumKit screen is never blank: when a region gets a skeleton rather than a spinner or nothing, the three shapes every loading state is built from, the fifteen family skeletons and what each keeps real, how many to show, when they appear and what replaces them, and the shimmer.
A screen is never blank
A screen is never blank. The page frame draws at once, real; a skeleton fills every region whose content is still on its way, at the size that content will take; and when the content lands, nothing moves.
Recent activity
What your team did this week.
The frame draws at once, and it is real
Skeletons fill the regions where data lands
The generic fallbacks cover everything else
Skeleton.TextBlock for any text region and Skeleton.Card for any tile stand in where no family skeleton exists, including a component built next year. Nothing is ever empty.Skeleton, spinner, or nothing
Is this text something the product wrote, or something the server sends? Written copy is drawn real. Server data gets a skeleton.
The situation | Reach for | Why |
|---|---|---|
| A region whose shape is known and repeats: rows, cards, a chart, a profile | Skeleton | The reader sees where things will be before they are there, and nothing moves when they arrive. |
| A control doing work the reader started: Save, Send, Upload | The control’s own loading state | Button isLoading blocks the press and says the wait where the reader is looking. |
| A wait with no known shape, or a region too small to say anything | Loading indicator | A ring is honest when there is nothing to draw the shape of. |
| Content the product wrote, or an instant load | Nothing | Copy, labels and icons ship with the page; a skeleton for them is a flash for no reason. |
Never a skeleton and a spinner together
What never gets a skeleton
These are chrome. They ship with the page and render as themselves while everything around them loads.
- Buttons, button groups, inputs, checkboxes, radios, toggles, sliders
- Tabs, breadcrumbs, filters, content dividers, section footers
- Date pickers, progress steps
- Alerts, banners, empty states, tooltips, badges and tags
- Written forms: sign-in, sign-up, verification, a password prompt, settings
- The frame of a modal, a slideout or a command menu: the title, the close button and the footer buttons are written; the body composes the skeletons of whatever loads inside it
The three shapes
Every skeleton is built from these, and only these. A Line for text, a Circle for an avatar, a Block for everything else, in one fill.
Line
Circle
xs 24, sm 32, md 40, xl 56; display is 160.
Block
Sized by class; radius matches what it replaces.
A Line stands in for text, at the text’s size
Skeleton.Line size names the text it replaces: xs 12px, sm 14, md 16, xl 20, display 30. The frame is that text’s full line height with a thinner bar centred in it, so swapping a text node for a Line moves nothing above or below. Set the width on the Line; it is full width by default.A Circle stands in for an avatar, at the avatar’s size
Skeleton.Circle size: xs 24px, sm 32, md 40, xl 56, display 160.A Block stands in for an image, a chart, a thumbnail or a badge
Skeleton.Block takes its size from className and matches the radius of what it replaces; a badge is a Block at the badge’s height with rounded-full.One fill everywhere
bg-quaternary, which flips in dark mode. No other grey, no border, no shadow. Never draw a fake trend line, a fake number or fake words: a shape the data then contradicts is worse than a plain block.Building a family skeleton
Start from the real component and swap out only what arrives. The activity feed row, loaded and loading, is the whole method.
The real row
Its skeleton
Start from the real component at its default variant
Swap each piece of arriving content for a shape
Keep what is known up front real
Change no height, padding or gap
One skeleton per layout, never per variant
How many
Enough to say what is coming, and no more.
Five rows, widths varied
Loading more keeps the rows
Added a file to Marketing site redesign
Invited Alisa Hester to the team
Invited Alisa Hester to the team
About five rows, or one screenful
Vary the line widths row to row
text in short, medium or long; cycle them.Loading more keeps the rows already showing
Timing, and when it fails
A skeleton appears at once, stays as long as the request takes, and is replaced by the content or by the error empty state. It never resolves on its own.
Recent activity
Added a file to Marketing site redesign
Invited Alisa Hester to the team
Invited Alisa Hester to the team
Show it at once
It never gives up on a timer
On failure, the error empty state with Retry
error empty state in the same region; something the reader did that failed is an alert. A skeleton that pulses forever is a blank screen with better manners.Load progressively: chrome, then text, then media
Motion
One animation on every shape, and a flat fill for readers who have asked for less.
A light band sweeps left to right
--animate-skeleton-shimmer token: a two second cycle in which the band fades in as it enters, crosses at a steady speed, fades out before the far edge and rests off-screen before the next pass, so it never sits or snaps. It says the page is alive without drawing attention to any one shape.Content rises into place
Skeleton.Reveal: a 1200ms rise of 8px with a fade, on an easing that lands softly. The skeleton already held the footprint, so the rise is small and nothing around it moves. Never fade the skeleton out on its own; the swap is the transition.Reduced motion stops it flat
prefers-reduced-motion the band does not move, the flat fill stays, and content appears without the rise or the fade. The region’s status text carries the wait for those readers.Accessibility
A screen reader hears one thing: the region, once. The shapes are decoration.
Wrap every skeleton in a Region
Skeleton.Region is a polite status marked busy that names the wait once (“Loading”, or label for something more specific). It is the one part a screen reader hears.The shapes are silent
aria-hidden. Shapes have no role, no name and no focus; a skeleton is not a control.Replace the whole region
In practice
Six calls that come up on every screen that loads.
Skeleton the data, not the chrome
Do
Members
Don’t
A toolbar, a title the product wrote and a row of buttons are known before the request is sent. Drawing them as shapes hides the one thing a reader could already use.
Match the real footprint to the pixel
Do
Don’t
The skeleton takes the component’s own height, padding and gaps. A near miss jumps when the content lands, which is the thing a skeleton exists to prevent.
Shapes, never fake content
Do
Don’t
A drawn trend line, a placeholder number or lorem ipsum makes a claim the data then contradicts. A block says only that something is coming.
A screenful, not a scroll
Do
Don’t
… and 47 more
Five rows say a list is coming. Fifty rows say the page is broken, and they scroll past anything else the reader could look at.
Fail to the error state, never pulse forever
Do
Activity didn’t load
Check your connection and try again.
Don’t
still going, a minute later
When the request fails, the region becomes the error empty state with Retry. A skeleton that never resolves is a blank screen that looks busy.
One mark per wait
Do
Don’t
A spinner inside a skeleton, or a skeleton behind a spinner, says two things about one wait. Pick by the decision table and stop.
Every family’s skeleton
One per data-bearing family, drawn on that component’s own page beside the real thing. Each card says what stays real.
Before you ship
What a review checks on any screen that waits.
- Every region that waits on a request has a skeleton or a generic fallback; the frame around it is real.
- The skeleton is the family’s own, or Skeleton.TextBlock or Skeleton.Card where none exists.
- Each skeleton sits inside a Skeleton.Region with a label that names the wait.
- About five rows, line widths varied, and loading more keeps the rows already showing.
- The skeleton shows at once and stays until the request returns; on failure the region shows the error empty state with Retry.
- No spinner shares the region with a skeleton.
- Checked with reduced motion on: the fill is flat and the status text still names the wait.
The engineering side, the props and the wiring, is on the Implementation tab.