Components/Feedback
Skeleton
Skeletons stand in for content that has not arrived yet, drawn in roughly the shape it will take. They tell the reader that something specific is coming and where it will land, so the page settles into its final layout instead of jumping once the content appears.
import { Skeleton } from '@growth-protocol-ai/alpha';When to use
Reach for a skeleton when you already know the shape of what is arriving and the wait is long enough for a reader to notice it — a card, a row of a table, a paragraph of copy. Draw it at the size and the count the real content will take: the habit that most often goes wrong is a placeholder that is only roughly right, which buys a second layout shift rather than saving the first.
When not to use
Do not reach for a skeleton when the shape of the response is unknown — a save, a search that may return anything, a request whose result you cannot predict. Spinner is the component for that, and what separates the two is what you can honestly promise: a spinner says only that something is happening, where a skeleton says what is coming and where it will land. A surface with nothing on its way at all is neither, and belongs to EmptyState.
variant3 values
What picks the variant is the shape of the thing being waited on, not how long the wait is. Reach for one only where that shape is actually known — a skeleton is a promise about what is coming, and one that guesses wrong is worse than a ring that says nothing.
| variant | When to use |
|---|---|
text | A stack of bars standing in for lines of copy. The default, and the only variant lines applies to. |
block | One box at the width of its container and the height it is given — a chart, a thumbnail, a panel with a fetch of its own. It carries a floor of its own, so a block inside a container with no height is still visible. |
inline | A short run inside a sentence, at the height of the text around it rather than the height size would give it. For the one value a paragraph is waiting on, where a bar on its own line would break the prose. |
size5 values
Each size is a step of the type scale rather than a pixel count: the bar takes a font size, and the gap between bars is whatever leading is left over from the line height the scale pairs with it. The pairing is not always the same-named token — lg is H4 at 18/22, and takes --line-height-sm.
| size | When to use |
|---|---|
xs | 12px on a 16px line. Body Small and Caption — table cells, metadata, a dense list row. |
sm | The default. 14px on a 22px line: Body Medium, which is the copy most of a product is written in. |
md | 16px on a 24px line. Body Large and Sub2, for a lead paragraph or a roomier layout. |
lg | 18px on a 22px line. H4 — a section heading above a stack of body bars. |
xl | 20px on a 24px line. Sub1, the largest step here, for the title of a card or a panel. |
| State | When to use |
|---|---|
One bar | The default, and what a value, a label or a one-line field waits on. A lone bar is never shortened: there is no paragraph for it to be the end of, so raggedness would be describing something that is not there. |
A paragraph | Several bars at one type step, the last of them cut to 70% so the block reads as prose rather than as a barcode. Because the gap between bars is that step’s own leading, they occupy exactly the lines the copy will, and nothing moves when it lands. |
Put aria-busy on the region that is loading and leave the bars hidden. They are aria-hidden by default because four of them would otherwise be four voices describing one wait — the same rule Spinner states, from the other side.
Match size to the type the bars replace. Getting it wrong gives up the only thing a skeleton buys over a spinner, and the mismatch is invisible until the content lands.
Widths per bar are deliberately not a prop. A placeholder that mimics the exact text reads as uncanny rather than helpful, and a shortened last line is all the raggedness a paragraph needs.
Size a block from outside, through [data-slot="skeleton"] — the same selector a real application gets. There are no width or height props, so one page cannot end up with three differently sized placeholders for the same thing.
Do not paint the bars. They are a tint of the text colour rather than a step of the neutral ramp, so they take their contrast from whatever surface they are dropped on and follow a retint on their own — a hard-coded fill is the one thing that can make one vanish again. This is also why there is no colour prop and no selector on a bar: the root carries data-slot, the bars inside it do not.
To hold one still, reach the bars rather than the container: [data-slot="skeleton"] > * { animation: none }. The container declares no animation, so a rule on it alone cancels nothing. There is no prop for it: prefers-reduced-motion is already honoured, and there the sweep slows rather than stopping, because a still bar reads as content that has arrived.
There is no circle and no avatar variant. Alpha publishes nothing round for one to stand in for, and it gets one when that case is concrete rather than anticipated.
Placeholder means something else here. EmptyState’s placeholder type is a surface that has not been built yet; this is a surface that is on its way. Do not describe a skeleton as one.
Standing in for a real layout
A card that is waiting, beside the same card once it has arrived. A heading is a second Skeleton at a larger size rather than a narrower first bar, which is what keeps the two frames the same height — the test of a placeholder is that nothing on the page moves when the content replaces it.
Four suppliers account for 61% of APAC spend, up from 48% at the start of the quarter. Two of them share a single tier-2 source.
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | "text" | "block" | "inline" | "text" | The shape being stood in for. See the variant table above. |
size | "xs" | "sm" | "md" | "lg" | "xl" | "sm" | Which step of the type scale a bar stands in for. Ignored by inline, which takes its height from the surrounding text. |
lines | number | 1 | How many bars, for text. The last of several is shortened; anything below 1 is treated as 1. Every variant honours it, so a block or an inline skeleton with lines above 1 draws that many — leave those two at 1 unless you mean it. |
aria-hidden | boolean | "true" | "false" | true | Scaffolding is not content, so the bars stay out of the accessibility tree. Overriding it exposes a span with no role and no name, so it is only useful alongside a role and a label you supply yourself. |
children | never | — | The bars are drawn from lines, so there is nothing to put inside one. What is loading goes around the skeleton, not in it. |