Summary#
A Slot is the point where a component renders the children its caller passed it — the content projection
mechanism (React's children, Vue's <slot>). A component that writes Slot in its render tree is a wrapper:
its caller supplies a content block, and the wrapper decides where that content lands.
[Composable] component Card() {
render { Box { Slot; } } // whatever the caller wraps in a Card lands here, inside the box
}Calling Card { Text("hi"); } renders Text("hi") inside Card's box. The card frames the content; it never
needs to know what the content is.
Signature#
Slot; // the default slot — renders the caller's children block here
Slot("header"); // a NAMED slot — renders the caller's `slot header { … }` fill here
Slot(id: field); // …and NAME what lands here, so the wrapper's own caption can point at itA caller passes the default content by writing a content block after a component call, and fills a named
slot with slot <name> { … }:
Card { // the block below is Card's default children
slot header { // fills Card's Slot("header")
Text("Title");
}
Text("body"); // untagged → Card's default Slot
}Description#
A component call takes an optional trailing { … } children block. Those child nodes are handed to the called
component as its slot content. Wherever that component writes Slot, the caller's children render in place.
The projected content evaluates in the caller's scope, not the wrapper's. This is what makes wrappers reusable: the content can read the caller's own state, props, and loop variables — the wrapper only positions it.
[Composable] component Panel() {
render { Box { Slot; } }
}
[Composable] component Greeting(string who) {
render {
Panel { Text(who); } // `who` is Greeting's prop — resolved in Greeting's scope, not Panel's
}
}A wrapper that writes Slot but whose caller passes no children renders nothing at that position (never an
error). A component that never writes Slot simply ignores any children a caller passes.
Named slots#
A wrapper with more than one insertion point gives each a name: Slot("header"), Slot("footer"). A caller
fills a named slot with a slot <name> { … } block; untagged children still fill the default Slot.
[Composable] component Card() {
render {
Box {
Slot("header"); // the header region
Slot; // the default region
}
}
}
[Page("/")] [AllowAnonymous] [Render(CSR)]
component Home() {
render {
Card {
slot header { Text("Title"); } // → Card's Slot("header")
Text("body"); // → Card's default Slot
}
}
}The slot name is checked at compile time: filling a slot the wrapper doesn't declare (a typo, or a slot that doesn't exist) is a compile error, so a mis-named fill is caught before it ships. An editor completes the available slot names from the wrapper you're calling. A named slot the caller doesn't fill renders nothing (it's optional).
Slots render identically whether a route is delivered server-side (pre-rendered HTML) or client-side, so a page built from wrappers hydrates without a flash.
Naming what lands in a slot#
A wrapper renders the caption and the caller renders the control, on two sides of a boundary. So a form component looks perfectly labelled and is labelled for exactly one audience: people who can see the layout.
Slot(id: <handle>) closes that in the one direction that is well-defined — the wrapper declares the handle and
claims the content it is filled with. The caller writes nothing:
[Composable] component Field(string label) {
render {
Stack(gap: 1) {
Text(label, labelFor: field); // a real <label>, and it names…
Slot(id: field); // …whatever the caller puts here
}
}
}
[Page("/signup")] [AllowAnonymous] component SignUp() {
string name = "";
render { Field("Your name") { Input(value: name); } }
}The caption is written once, at the call site, and the pair is a real <label for> — so clicking the words
focuses the field, which on a form of small controls is most of the hit area. Writing label: "Your name" on the
Input instead gives an accessible name and neither of those: the caption is then written twice, with nothing
keeping the two in step.
⚠ The fill must be a single element. for= names one element; with two roots there is no answer, and the first
one takes the name. Where a slot legitimately holds several things, name the control directly with label:.
Examples#
A reusable card wrapper framing page-specific content:
component Card() {
render { Box { Slot; } }
}
[Page("/welcome")]
[Render(SSR)]
component Welcome() {
render {
Card {
Text("Welcome");
}
}
}A wrapper with a named region plus its default content — the caller fills the named slot by name (a mis-typed name would be a compile error) and leaves the rest for the default slot:
component Panel() {
render {
Box {
Slot("header");
Slot;
}
}
}
[Page("/article")]
[Render(SSR)]
component Article() {
render {
Panel {
slot header { Text("Title"); }
Text("Body");
}
}
}See also#
- component — the component archetype a slot lives in
- layout primitives — the layout atoms (
Box,Stack,Row) a wrapper frames its slot with - routes and pages — binding a wrapper-composed page to a route