Slots
A slot is a hole in a component that the parent fills with its own HTML.
Props pass data down; slots pass markup down. That is what lets one component wrap content it knows nothing about.
Default Slot
Put <slot /> in the component, where the content should
land:
<!-- Card.html -->
<div class="card">
<slot />
</div>
<style>
.card {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 1rem 1.25rem;
}
</style>Whatever you write between the component's tags fills that slot:
<Card>
<strong>Storage</strong>
<p>You have used 4.2 GB of your 10 GB plan.</p>
</Card>
You have used 4.2 GB of your 10 GB plan.
Default Content
Content written inside <slot> is a fallback. It renders only
when the parent passes nothing:
<!-- Button.html -->
<button class="btn">
<slot>Click me</slot>
</button>
<style>
.btn {
background: var(--accent);
color: var(--bg-primary);
border: 0;
border-radius: var(--radius-sm);
padding: 0.5rem 1rem;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.btn:hover {
background: var(--accent-muted);
color: #ffffff;
}
</style><!-- Nothing passed, so the fallback shows -->
<Button />
<!-- Content passed, so it replaces the fallback -->
<Button>Submit</Button>
Named Slots
Give a slot a name when a component has more than one hole. Here
is the same card, grown to a header, a body and a footer:
<!-- Card.html -->
<div class="card">
<header class="card-header">
<slot name="header">Untitled</slot>
</header>
<div class="card-body">
<slot />
</div>
<footer class="card-footer">
<slot name="footer" />
</footer>
</div>
<style>
.card {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.card-header {
display: flex;
gap: 0.5rem;
align-items: baseline;
padding: 0.625rem 1.25rem;
border-bottom: 1px solid var(--border);
font-weight: 600;
}
.card-body {
padding: 1rem 1.25rem;
}
.card-footer {
padding: 0.625rem 1.25rem;
border-top: 1px solid var(--border);
color: var(--text-secondary);
font-size: 0.875rem;
}
</style>
The parent aims content at a named slot with the slot attribute.
Anything without one goes to the unnamed slot:
<Card>
<span slot="header">Storage</span>
<p>You have used 4.2 GB of your 10 GB plan.</p>
<span slot="footer">Updated a moment ago</span>
</Card>
You have used 4.2 GB of your 10 GB plan.
Modals, drawers and popovers are built in and already handle layering, focus and dismissal, so reach for those rather than building your own from slots - see Overlays & UI State.
Multiple Elements to Same Slot
Several elements can target one slot. They arrive in the order you wrote them:
<Card>
<span slot="header">Storage</span>
<span slot="header">Pro plan</span>
<p>You have used 4.2 GB of your 10 GB plan.</p>
<span slot="footer">Updated a moment ago</span>
</Card>
You have used 4.2 GB of your 10 GB plan.
Slot Props
A slot can hand values out to the content that fills it. Write them as
shorthand attributes on the <slot> tag:
<!-- Card.html -->
<script>
let plan = 'Pro'
</script>
<div class="card">
<header class="card-header">
<slot name="header" {plan} />
</header>
<div class="card-body">
<slot />
</div>
</div>
Any component placed in that slot receives them as ordinary props:
<Card>
<!-- PlanBadge receives plan="Pro" from the header slot -->
<PlanBadge slot="header" />
<p>You have used 4.2 GB of your 10 GB plan.</p>
</Card>
What a slot offers reaches that slot's content, and nothing else in the file. Inside it, two things receive the value:
-
A component projected into the slot.
<PlanBadge />getsplanas if it had been written on the tag, so alet planit declares is filled in rather than defaulted. -
An expression written in the content.
{plan}beside the tag reads the same value, because the offer is layered over your own names for the length of that slot's content.
Changed in 0.1. An offer used to reach component tags only, so an expression that named one rendered empty. Both halves are compiled now.
A value written at the call site (<PlanBadge plan="Free" />) wins over
the offer, and a name the declaring component has no value for leaves the consumer's own
default alone.
Unlike ordinary props they keep their JavaScript type instead of arriving as strings. Every
other prop is an attribute and therefore a string; this one is the declaring component's own
value, so a number arrives as a number, an array as an array, and a function as something
the content can call. Calling one is the usual way a projected component talks back - an
<Accordion> that offers {open} and {toggle}
lets its projected header drive it.
What Projected Content Resolves Against
Content you write inside a component tag is yours. It is compiled as part of the
template that wrote it, and it lands in the child's <slot /> position -
so its names are your component's names, not the child's.
| In projected content | What happens |
|---|---|
{count} and other text expressions |
Resolve against your state |
An inline handler, onclick="save()" |
Compiles into your handler table |
An expression prop on a projected component,
<Tally total={count} />
|
An ordinary reactive prop: it updates when count does.
Changed in 0.1 - it used to be interpolated once at mount and never
again
|
A <for> or <show> over your own state |
Renders your list, in the child's element. Changed in 0.1 - it used to be mounted by the CHILD, against state where your variable did not exist, so it silently rendered nothing |
A name the slot offers, {plan} inside the content of
<slot {plan} />
|
The declaring component's value, ahead of a name of your own spelled the same way. Changed in 0.1 - an offer used to reach component tags only, so an expression naming one rendered empty |
An attribute expression on an element,
<p class="{cls}">
|
A build error. Put the element in a component of your own, or pass the whole class string as a prop |
A directive - class:, style:, bind:,
ref:, on:, use:
|
Dropped, with a [jaf] warning naming it |