Skeleton

The shape of the content before the content arrives, so the layout does not move when it does.

Usage

When to reach for Skeleton, what each of its props is for, and the choices it leaves to you.

When to use

A skeleton is the shape of the content, drawn before the content arrives, so the layout does not move when it does. Draw one when you know that shape and the wait is long enough to see.
  • A first load of a layout you know in advance: a profile card, a list row, a table body, a dashboard tile.
  • A wait long enough to notice, about a third of a second and up, that you cannot put a number on.
  • A surface where the placeholder can sit in the same box the content will land in, with the same padding and the same container.
What to reach for instead.
  • Nothing at all under about 200 milliseconds. A placeholder that appears and disappears inside a quarter of a second reads as a flicker, which is worse than the wait it hides.
  • Spin when the wait is short, or when you do not know the shape: a dialog opening, a search re-running, a save.
  • Progress when the wait is measurable: an upload, an export, a job with steps. A skeleton cannot say how far along anything is.
  • The control's own busy state for something the reader just did. A Button with loading already says it, next to the thing that caused it.
  • Empty or Result for a wait that ended with nothing, or ended badly. A skeleton that never resolves looks like a broken page.
  • Table's own loading when the surface is a Table. It shows a spinner over the rows. Build a grid of Skeleton.Input bars only for a data grid that is not a Table.
A refresh of content already on screen is usually not a skeleton either. Replacing what the reader is reading with gray bars loses their place. Dim the old content and leave it there.

Match the real shape

A skeleton helps only when its box is the box the content will occupy. Get the shape wrong and the layout jumps twice: once when the bars appear, and again when content of a different height replaces them.
  • Draw the number of lines the content usually has, not the most it could have. A two-line summary drawn as five rows drops everything below it by three lines when the data lands.
  • Put the placeholder inside the same card, the same padding and the same grid cell as the content. Space reserved beside the box does not help.
  • If the real row has an avatar, the skeleton has an avatar. avatar defaults to false, so it is the part most often forgotten, and it is the part that sets the row's height.
  • A bare <Skeleton /> is a title plus three body lines, because title and paragraph both default to true. That is a default, not a measurement of your content.
  • Where the height cannot be known ahead (a chart, a map, a video frame), fix the height yourself and use Skeleton.Node instead of guessing at a row count.

Wrap the content, do not branch

loading is the switch and children is the real content. With children, Skeleton shows the placeholder while loading is true and the children once it is false. That leaves one component at the call site instead of a ternary between two trees you have to keep the same size by hand.
  • Leave out loading and the placeholder always shows. That is how a standalone placeholder works, and it is also why a misspelled prop name leaves a page stuck on the bars with no warning.
  • loading={false} with no children renders nothing. Use it when the placeholder and the content are siblings.
  • A loading Skeleton does not render its children. A Skeleton.Button placed inside it never shows. Put the button beside the Skeleton and swap it for the real one on the same loading flag, as the List example does.
  • Render the parts yourself only where the placeholder cannot be the same subtree as the content: rows inside a table body, cells inside a grid. Everywhere else, wrapping means fewer moving pieces.
  • Keep the props that describe the shape on the Skeleton. avatar and paragraph={{ rows: 2 }} beside loading describe the whole placeholder in one place.

Title and paragraph

The root Skeleton draws a text block: one title bar and a list of body lines. title and paragraph each take true, false or a config, and false removes that part.
  • paragraph.rows counts body lines only. Three rows under a title is four bars on screen.
  • The default row count depends on what else is present: three with a title and no avatar, two otherwise.
  • paragraph.width takes one value, which applies to the last line, or an array with one value per line. A short last line makes the block read as prose.
  • title.width takes a number or a string. Without one, the title is 38% wide beside a paragraph, 50% beside a paragraph and an avatar, and full width on its own.
  • round turns the title and the body lines into capsules and leaves an avatar beside them alone. Pick one style and keep it across the page.

The avatar

avatar is off by default. Pass true for the default avatar, or a config when the real avatar differs from it.
  • The default shape depends on what else is present: square when there is a title and no paragraph, a circle otherwise, and size: "large" either way.
  • Match the real avatar's size. The avatar is the tallest thing in most rows, so it decides whether the row jumps.
  • The config takes size and shape but not active. The root's active shimmers the avatar, the title and the paragraph together.
  • Use a standalone Skeleton.Avatar when there is no text block beside it. The root Skeleton with title={false} and paragraph={false} works too, with more props to read.

The five shapes

Beside the text block, Skeleton has five element placeholders. Each is one shape, for when the thing you are reserving space for is a single control.
  • Skeleton.Button is twice as wide as it is tall and takes size, shape and block. Use it for toolbars and card footers.
  • Skeleton.Input is five times as wide as it is tall and takes size and block but no shape. A form skeleton is a stack of these, and a table skeleton is a grid of them.
  • Skeleton.Avatar is the avatar the root draws, on its own, with shape limited to circle or square.
  • Skeleton.Image is a square box with a picture glyph inside it. The glyph is part of the component, so switching icon packs does not change it.
  • Skeleton.Node is a gray box with whatever you put inside it, for the shapes the other four do not cover.
  • Size Image and Node through their content. Neither takes size or shape, and className lands on the outer element, so it does not resize the gray box. Use classNames={{ content: "size-32" }} or style.
  • Write size="medium" for the middle size. size="default" still works but warns in development.
Each element takes its own active. Nothing passes from a root Skeleton to an element beside it, so set the flag on each one from the same piece of state.

How much to draw

Draw about as much as the page will show, and no more.
  • About a screenful. Placeholder rows below the fold cost layout and paint for something nobody is looking at.
  • Keep the count fixed between loads. A count taken from the last response makes the placeholder a different height every time, which is the jump you were avoiding.
  • Draw the shape of a region, not every detail in it. A form skeleton is a few label and field bars, even when the form has a dozen controls.
  • Do not stagger them in. Rows appearing one at a time read as content arriving, and the reader waits for the rest.
  • Where a list has a known page size, draw that. Where it does not, three to five rows say “a list is coming” without claiming how long it is.

Active and the shimmer

active runs a gradient sweep across every bar for as long as the placeholder is on screen. It tells the reader the page is still working.
  • Turn it on for anything in flight. A static skeleton is easily read as content that failed to load, and a reader who thinks the page is finished reloads it.
  • Leave it off for a placeholder that only reserves space in a design: an empty state, a disabled region, a print layout.
  • The shimmer does not check prefers-reduced-motion, and turning motion off in the theme does not stop it. Pass active={!reduced} from your own check. The Accessibility tab has more on this.
  • Do not add a second animation on top. A placeholder that shimmers, fades and slides competes with the content it stands in for.