Skeleton
A grey box standing in for something that has not arrived — three utilities and nothing else.
src/components/rahti_ui/skeleton.rs
The skeleton
An avatar and two lines, which is shadcn's first example.
shadcn's own demo. The whole component is `animate-pulse rounded-md bg-muted` — everything else you see is the `class` on each box, because a placeholder's job is to be the shape of the thing it stands in for.
There is no variant, no size prop and no script. It is the smallest file in this library and the only one whose entire design fits on one line.
Text
A paragraph before it arrives.
Three lines, the last one short — which is what a paragraph looks like from a distance, and the reason a text skeleton reads as text rather than as three bars.
Avatar
A round one beside two lines.
`rounded-full` here is the one kind of override that actually contends with the component's own utilities — it has to beat `rounded-md`. That is resolved by `twMerge` in the browser, which is what the section below is about.
`shrink-0` is on the circle rather than the text, because a flex item with no explicit basis will happily give up its width to a longer neighbour — an avatar that squashes into an oval is the usual way this arrangement goes wrong.
In a card
A header and a media block.
shadcn's card example. The skeleton takes the card's own spacing — nothing is passed between them, because a grey box is just a box and the card lays it out like any other child.
`aspect-video w-full` is a placeholder that keeps its shape at every width, which is what stops the layout jumping when the real image lands.
Form
Labels, fields and a button.
Two labelled fields and a button. `h-4` for a label and `h-8` for a control is the whole trick — the sizes are the real components' sizes, so the page does not resize when they arrive.
Table
Five rows, three columns.
Five rows of three. `flex-1` on the first cell and fixed widths on the other two is what gives the column edges a straight line — the same thing a real table would do.
Before the runtime
The one place this library's class merge has a real cost — and the way out.
These three look identical to the ones further up, and they are not the same markup. A caller's `class` cannot be resolved on the server — there is no Rust tailwind-merge — so `<Skeleton class="h-4 w-full" />` is served with `class="{twMerge(…)}"` and becomes a real class list when PulsePoint mounts.
For a Badge that costs a moment of the wrong colour. For a skeleton it costs the whole element: with no `h-` and no `w-` there is nothing on screen at all, and the thing whose entire job is to hold a shape before anything loads is the thing that is not there yet.
`skeleton_attrs("h-4 w-full")` joins the same layers on the server and hands back a finished attribute set, so these three are painted the moment the HTML arrives. The join does not resolve conflicts, so it is exact for any size or position — and not for a second `rounded-*`, which would sit beside `rounded-md` rather than beat it. Use the component for those.
Announcing it
The loading state is a property of the region, not of the grey boxes.
shadcn puts no `role` and no `aria-*` on a skeleton, and this port does not either. That is the right call: a placeholder is decoration, and what a screen reader needs to know is that the *region* is loading.
So the state goes on the container — `aria-busy="true"` while the content is on its way, and `aria-live="polite"` if the arrival should be announced. That belongs to the page, which is why no prop was invented for it — the same call the Progress's `aria-labelledby` note makes.
The port
One token, one addition, and one thing deliberately left alone.
Four components in a row where the published registry and the published page disagree — a rebuilt Progress, a restyled Alert and Badge, and now a single token. Worth checking every time rather than trusting either source.