Skip to content

Skeleton

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/skeleton.json
See it liveA paragraph, a list, two grids, and single blocks.→

The rule it is built on:

A skeleton of the wrong shape is worse than no skeleton.

It promises something the content does not keep, and the promise is paid for in a jump: the page settles, the scrollbar appears, and whatever the reader was about to click has moved. Measured rather than assumed — the line’s own calendar drew four short lines where a six-row month grid was about to land, and the skeleton was itself the jump it existed to prevent.

So the useful part is not <Skeleton /> — that is four lines anyone can write — but the shapes. They are what a call site reaches for, and what keeps the placeholder honest.

{isPending ? <SkeletonList rows={6} /> : <Works rows={works} />}

A spinner is the right answer when the shape is not known. A skeleton claims to know; if it does not, say less rather than more. See Spinner, and Progress for the unknown-fraction case.

The widths vary, and that is deliberate. A stack of equal bars reads as a loading indicator; ragged ones read as text. They rotate through a fixed set rather than being random, because a placeholder that changes between renders flickers — and a placeholder is on screen exactly when things are re-rendering.

Everything here is aria-hidden. The loading fact belongs to the region and is said once, by whatever owns it — QueryState does it with aria-busy. Announcing a dozen empty boxes as content would be noise on top of a fact the reader already has.

A count of zero draws one. rows={works.length} before anything arrives is zero, and an empty box where a placeholder belongs is the jump the component exists to prevent.

Props

Read from the types in ui/skeleton.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

Skeleton

One block. Size it with className - a skeleton is a shape, and the shape belongs to whatever it stands in for.

Every prop of React's HTMLAttributes<HTMLDivElement>, passed on.

SkeletonText

A paragraph's worth of lines.

PropTypeDefault
linesnumber3How many lines of text stand here.

And every prop of React's HTMLAttributes<HTMLDivElement>, passed on.

SkeletonList

Rows of a list, which is the shape most screens are waiting for.

PropTypeDefault
rowsnumber5How many rows of the list stand here.
secondarybooleantrueDraw a second, shorter line under each row - for a list whose rows carry a title and something beneath it.

And every prop of React's HTMLAttributes<HTMLDivElement>, passed on.

SkeletonGrid

A grid of cells: a gallery, a board, a month.

PropTypeDefault
cellsnumber12How many cells.
columnsnumber4How many per row.
cellClassNamestring'aspect-square'The aspect of one cell, as a Tailwind class - a gallery of covers is not shaped like a grid of tiles.

And every prop of React's HTMLAttributes<HTMLDivElement>, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 5 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/skeleton.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.37.0
    • fix give every loop a rest of its own under reduced motion 18511f0
  3. v0.23.0
    • feat the screens between asking for data and showing it bc3f636