Summary#
A component that reads data has a moment before that data arrives. skeleton { } says what to draw in that moment:
the shape the content will take, at the size it will take, so the page is composed before it is filled.
It is an ordinary render tree — same grammar, same controls, same style props — held on the component beside
render { }. The runtime shows it while a query of that component has never settled, and swaps it for the real
tree the moment the first result lands.
Without one, a component whose read is in flight renders as nothing, and the page assembles itself in front of the user as each piece arrives. That is not merely unpolished: content that appears late pushes what is already on screen, so a person reading — or aiming at a button — has it move under them.
Signature#
component Name(<params>) {
live var <data> = …; // the read the skeleton stands in for
render { … } // the real tree
skeleton { … } // the stand-in — same grammar, no data reads
}At most one skeleton block per component, and it is optional everywhere.
Description#
It fills a CHILD's window, not the page's#
A routed page awaits its own data before the first paint, so by the time the page exists its queries have already answered — a page-level skeleton would have nothing to cover. The window this block fills belongs to a composed child: a child's read is deliberately not awaited, so the child paints and then fills. That is the ordinary case for a kit control or any component you drop into a page.
So put skeleton on the component that does the reading, not on the page that contains it. A component with no
queries never shows a skeleton, because there is nothing to wait for.
One consequence worth knowing before you hit it: because a skeleton belongs to a composed child, that child needs
[[ui-composable|[Composable]]] when the page around it is public. Its own read stays gated either way — the
attribute is about being allowed to render inside an anonymous page, not about what it may read.
It shows on the FIRST read, and never again#
The swap is driven by whether a query has ever settled — not by whether one is currently loading. Those differ exactly once, and it matters:
| First read, nothing on screen yet | the skeleton shows |
| A refetch, with the previous rows still on screen | the skeleton does not show |
Replacing rows the user is already reading with grey placeholders is a regression, not a loading state. For the in-flight feedback that a refetch or an action deserves, see Pending; for a read that came back refused or broken, see A failing query — that region reports the failure in place, and a skeleton would sit there forever pretending it was still coming.
A skeleton may not read the data it stands in for#
Reading a live var from inside skeleton { } is a compile error naming the member. This is not a style rule —
it is the block's defining condition. A skeleton renders precisely because that read has not arrived, so anything it
reads off it is empty by construction, and the placeholder would silently render nothing.
Params and plain var state are allowed, deliberately. Both are present the moment the component mounts, and a
skeleton that knows how many rows to draw is a better skeleton than one guessing three.
The check covers the two ways data reaches a render tree: element arguments and foreach sources. A data read
buried in an if condition is not caught — it renders as a false branch rather than as anything harmful.
Draw the shape, at the real size#
The bar to aim for is that nothing moves when the data arrives: the skeleton occupies the same box the loaded content will. A placeholder that is the wrong height is worse than none, because it promises a layout and then breaks it — the reflow it causes is the exact problem a skeleton exists to prevent.
In practice that means fixing the dimensions rather than letting a placeholder collapse: give each stand-in row the height its real row will have, and the container the gap it will have.
A skeleton is static by default. If you want the shimmer, it is an ordinary animation applied
with a style prop — the platform ships no privileged pulse, because the timing is a house-style decision.
It survives an export#
skeleton { } persists into the application model as a second render tree and is regenerated by the decompiler, so a
component exported and recompiled keeps it. Worth stating only because it did not always: the block was persisted and
read back by nothing for its whole first life, which is invisible in the output — the component still renders, just
with nothing on screen while its first read is in flight, which is the entire point of the block.
Examples#
A child that holds its shape while its rows load#
entity Order { [Required] string Reference; decimal Total; }
[Composable] // it is dropped into a public page; its own read stays gated
component RecentOrders() {
live var orders = Order.OrderByDescending(o => o.Total).ToList();
render {
Stack(gap: 2) {
foreach (var o in orders) { Text(o.Reference); }
}
}
skeleton {
// The same frame, with the row's real height — so nothing reflows when the rows arrive.
Stack(gap: 2) {
Box(h: "14px", w: "240px");
Box(h: "14px", w: "240px");
Box(h: "14px", w: "240px");
}
}
}
[Page("/orders")]
[Render(CSR)]
[AllowAnonymous]
component OrdersPage() {
render { RecentOrders(); }
}Sizing the stand-in from a param#
A param is present at mount, so the caller can tell the skeleton how much to draw:
component RecentOrders(int rows = 3) {
live var orders = Order.OrderByDescending(o => o.Total).Take(rows).ToList();
render { foreach (var o in orders) { Text(o.Reference); } }
skeleton { foreach (var i in placeholders) { Box(h: "14px", w: "240px"); } } // `rows` and plain state are fine
}What the compiler refuses#
component RecentOrders() {
live var orders = Order.ToList();
render { foreach (var o in orders) { Text(o.Reference); } }
skeleton {
foreach (var o in orders) { Box(h: "14px"); } // ERROR: a `skeleton` block cannot read 'orders' —
} // it renders precisely while that data is still loading
}See also#
- Pending — the in-flight feedback for actions and refetches, which is the other waiting state.
- A failing query — when the read comes back refused or broken, that region says so in place.
- animation — looping motion with no destination state —
animationblocks, if you want the stand-in to shimmer. - [Composable] — presentational components in public pages — why a composed child needs the attribute to render inside a public page.
- component — components,
render, and the members a skeleton may read.