Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

skeleton

component X(…) { render { … } skeleton { … } } — the shape a component holds while its FIRST read is in flight

A second render tree that stands in for a component while its first query has not yet arrived. It is written with the same grammar as `render`, it may not read the data it stands in for, and it gives way the moment the first result lands — never again on a refetch.

stable1 example compiled by CIuiloadingfeedbackcomponent

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 yetthe skeleton shows
A refetch, with the previous rows still on screenthe 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#

Related

Pending

When a control's action waits on the server, the platform shows a busy spinner and disables the control — but only…

A failing query

When a query a region reads fails or is refused, that region shows the failure in place — the server's own sentence…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

animation — looping motion with no destination state

An `animation` block declares reusable, looping motion — a shimmer, a pulse, an indeterminate progress hint. Its…

[Composable] — presentational components in public pages

Mark a presentational, composition-only component `[Composable]` so a public page can compose it without marking it…