Skip to content
Damask

Slots

Placement, fallbacks, filling with slot=, forwarding, asking, and filling from Rust.


A component places caller-supplied content with <slot>, and a caller routes content into a named slot with slot="…" — the same two halves web components use.

Damask
<!-- frame.dmk -->
<section>
  <h2>{self.title}</h2>
  <slot/>
  <footer><slot name="footer">© anon</slot></footer>
</section>

<slot> is only ever a placeholder. It marks where content lands; it never supplies any.

Slots are not fields. A template declares as many as it likes without the struct changing, and without the struct growing a Render type parameter for each.

Placement

Form Places
<slot/> the default slot, whose name is the empty string
<slot name="x"/> the slot named x
<slot name="x">…</slot> the same, with the body as fallback

name must be a plain string literal — it is resolved at compile time, so an interpolated one, which would name a different slot per render, is an error. So is an empty name: write <slot/> for the default slot.

Fallbacks

A <slot>‘s body is what renders when the caller leaves it unfilled. <slot/> with no body renders nothing. The fallback is the declaring template’s own markup, so it is laid out where it was written; a fill was written in the caller and is placed at the slot’s depth instead.

Filling

A direct child of a component element carrying slot="name" fills that slot. The element itself goes in — slot is a routing instruction, so it is consumed and never reaches the rendered markup or the child’s props. Like name, it must be a plain, non-empty string literal.

Damask
{use crate::ui::Frame}
<Frame title={self.heading.clone()}>
  <p>{self.body}</p>                          <!-- the default slot -->
  <span slot="footer">© {self.year}</span>
  <a slot="footer" href="/about">About</a>
</Frame>

A fill cannot contain .await, because it reaches the callee as a plain &dyn Render with no async render behind it. Compute the value above the tag and pass the result in — the error says so, and names the rewrite. A <slot>’s own fallback body has no such limit. See Async templates.

HTML
<section>
  <h2></h2>
  <p></p>
  <footer><span>© 2026</span><a href="/about">About</a></footer>
</section>

Several children may name the same slot. They land there in the order they were written, concatenated into one fill — which is why the footer above takes both the copyright and the link without either needing a wrapper of its own.

Content with no slot fills the default slot, whose name is the empty string — DEFAULT_SLOT — so a named slot can never collide with it. Whitespace alone does not count as content: a component tag whose body is only a line break fills nothing and takes the cheaper render_into path.

Because the element is part of the fill, a named slot always receives markup, not bare text. Reach for the element the content wants anyway (<span>, <li>, <footer>); the default slot, which takes children verbatim, is where loose text belongs.

slot is read only off a direct child of a component element. Everywhere else it is an ordinary attribute, so a template can still address the shadow slots of a browser-side custom element:

Damask
<my-card><p slot="footer">rendered as slot="footer"</p></my-card>

Forwarding

A component forwards one of its own slots by routing a placeholder: <slot> resolves against this component’s caller, and slot= hands the result to the child.

Damask
<!-- shell.dmk -->
<Frame title={self.title.clone()}>
  <slot/>                                   <!-- forward the default slot -->
  <slot name="footer" slot="footer"/>       <!-- forward "footer" -->
</Frame>

A bare <slot/> needs no slot=: with no name to route it to, it is ordinary default-slot content, so it forwards the default slot and can sit alongside other markup in the same fill.

Asking about slots

The caller’s fills are in scope as slots, for any { … } tag. It answers the question a fallback cannot: a fallback stands in for missing content, so it cannot say whether the markup around the content should exist at all.

Damask
<!-- dialog.dmk -->
<div class="dialog">
  <h2>{self.title}</h2>
  {#if slots.has_default()}<p class="body"><slot/></p>{/if}
  {#if slots.has("actions")}<footer>{@render slots.get("actions")}</footer>{/if}
</div>

Unfilled, that renders <div class="dialog"><h2>…</h2></div> — no empty <p>, no empty <footer>.

Both ways of placing a fill are there: <slot/> resolves implicitly, and {@render slots.get("actions")} does it by name. {@render} takes the Option as it comes — impl Render for Option<T> renders nothing for None — so the tag needs no guard of its own; the {#if} above is guarding the <footer>, not the fill.

Method Answers
slots.has(name) did the caller fill name?
slots.get(name) Option<&dyn Render> for name — renderable as-is
slots.has_default() did the caller pass any unslotted content?
slots.get_default() that content, if any

slots is an ordinary binding, so a template that wants the name for something else may shadow it; <slot> placement does not go through the binding and keeps working either way.

Matching is at render time

Important

Slots are matched by name, at render time. A misspelled name renders the fallback (or nothing) rather than failing to compile, and filling a slot the template does not declare is silently ignored.

This is the deliberate cost of keeping slots off the struct. If a slot is load-bearing, a test that asserts its content appears is the check the compiler is not giving you.

Filling from Rust

Rust
use damask::{Component, DEFAULT_SLOT, Renderer, Slot, Slots, fragment};

let body = fragment(|r: &mut dyn Renderer| r.write_raw("<p>hi</p>"));
let out = Layout.render_with(Slots::new(&[Slot::new(DEFAULT_SLOT, &body)]));

Slots::new takes a slice of Slots, each pairing a name with a &(dyn Render + Sync). The fills are borrowed, not owned: slot content stays on the caller’s stack and can borrow the caller’s data without boxing or type erasure. Slots is Copy and Default, and both constructors are const.

The Sync is there because an async template holds the whole Slots across each of its awaits, and a fill that could not be shared between threads would make that render future non-Send. It sits on Slot rather than on Render itself, so a component that neither fills a slot nor awaits — a generic one especially — is untouched.

Item Purpose
DEFAULT_SLOT the default slot’s name — the empty string
Slot::new(name, &render) one named fill
Slots::new(&[…]) the set passed to render_with / render_slots
Slots::EMPTY no fills; what render() and render_into pass
Slots::get(name) / has(name) the fill for a name, if any / whether there is one
Slots::get_default() / has_default() the same for the default slot
Slots::render(name, r, indent, fallback) what a lowered <slot> calls
Slots::render_async(name, r, indent, fallback) the same, in an async template — the fallback hands back a future

A name repeated in the slice resolves to the first entry. Slots::render is the one method a template’s <slot> compiles to: it applies indent to a fill, because that markup was laid out from the caller’s root, and calls fallback untouched, because that markup is already at the right depth.

Slots is the same type a template sees as slots, so these are the methods Asking about slots uses.

Slots vs. props

Use a prop for a value the component formats. Use a slot for markup the caller composes.

The distinction has a practical edge: because a label is a slot rather than a prop, a call site can drop the text at a breakpoint and leave an icon-only button, wrapping its own <span class="hidden lg:inline"> — without the component growing a prop for it.