Feedback
Skeleton
The wait, and the moment it ends. Most skeletons are a grey box with a shimmer; the interesting part is what happens when the content arrives.
Usage
import { Skeleton } from '@carabine/ui/skeleton';An array of widths is the bars — one entry each. A line count and a width are two ways of saying the same thing, and they disagree the first time somebody sets both.
The swap is the component
Both states live in one grid cell, stacked, so nothing reflows when they trade places. The placeholder leaves over 140ms with a blur; the content arrives over 220ms without one and with no entrance of its own. Out faster than in, and the two overlap — which is what makes it a crossfade rather than a flicker of empty space between them.
The bars are the placeholder, not the point. If you take one thing from this component, take the grid cell.
Examples
Shapes
circle for an avatar, ratio for anything with a known aspect, fill for a box that is already sized by its parent. Under a ratio the height follows the width, so the reserved space is the real space and nothing jumps when the image lands.
In a line of text
inline sits in a sentence instead of owning a block. It aligns on the baseline, which needed a nudge: a grid with no baseline-aligned items synthesises one from the border box, so the box's height was driving the alignment and the word climbed as the placeholder left.
alignItems: inline ? 'baseline' : undefined, // on the wrapper
transform: inline ? 'translateY(0.14em)' : undefined, // on the barDeployed to
Customization
<Skeleton
lines={3} // how many bars, when width is not an array
height={12} // one bar's height; the diameter when circle
last="62%" // the last bar's width, when there is a stack
gap={8}
radius={6}
shimmer={false} // no travelling light
placeholder={<YourShape />} // a shape of your own, instead of the bars
label="Loading the profile"
/>Styling reference
Tailwind utilities plus skeleton.css, which carries the shimmer's keyframe and its per-bar phase — --i on each bar, so the light crosses the stack at an angle rather than every bar flashing at once.
prefers-reduced-motion drops the shimmer entirely and keeps the crossfade: the travelling light is decoration, but the swap carries the meaning.
API reference
| Prop | Type | |
|---|---|---|
| className | string | Added to the component’s own classes, so yours wins |
| style | CSSProperties | Merged after the component’s own inline styles, so yours wins |
Colours are data and dimensions are props, and neither covers a margin, a font, or a class from your own system. That is what these are for. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.
Length is number | string — pixels, or any CSS length.
| Prop | Type | Default | |
|---|---|---|---|
| loading | boolean | true | false swaps to children |
| children | ReactNode | — | What is coming. Without it, this is a shape |
| placeholder | ReactNode | — | A shape of your own, instead of the bars |
| lines | number | 1 | How many bars |
| width | Length | Length[] | '100%' | An array is the bars, one entry each |
| height | Length | 12 | One bar’s height; the diameter when circle |
| last | Length | '62%' | The last bar’s width, when there is a stack |
| gap | number | 8 | Between the bars |
| radius | Length | 6 | |
| circle | boolean | false | One round bar, height across |
| ratio | number | — | 16 / 9. The height follows the width |
| fill | boolean | false | Take the parent, for a box that is already sized |
| inline | boolean | false | Sit in a line of text instead of owning a block |
| shimmer | boolean | true | The light travelling across |
| label | string | 'Loading' | Read out while the placeholder is up |
| theme | 'dark' | 'light' | 'dark' |