Skip to content

Slots

Slots let consumers inject content into a component’s layout. ElementsKit supports two approaches: native <slot> (browser-managed, Shadow DOM only) and Slot (ElementsKit-managed, works with or without Shadow DOM).

Native slots β€” Shadow DOM

When an element uses a shadow root, the browser projects slotted children into named <slot> placeholders automatically. The children stay in the light DOM β€” they are only visually projected.

class CardElement extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
// Three slots: named "header", unnamed default, named "footer"
shadow.innerHTML = `
<article>
<header><slot name="header">Untitled</slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</article>
`;
}
}
customElements.define("x-card", CardElement);

Consumer HTML:

<x-card>
<h2 slot="header">My Card</h2>
<p>This goes in the default slot.</p>
<button slot="footer">Close</button>
</x-card>

Consumer JSX (ElementsKit):

<x-card>
<h2 slot="header">My Card</h2>
<p>This goes in the default slot.</p>
<button slot="footer">Close</button>
</x-card>

The standard slot HTML attribute routes each child into the matching named slot. The browser handles projection with no extra JavaScript.

Shadow DOM slot with JSX template

Use JSX instead of innerHTML to build the shadow tree β€” the <slot> elements work the same:

import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
import { render } from "elements-kit/render";
@attributes
class CardElement extends HTMLElement {
static [attr] = {
title(this: CardElement, value: string | null) {
this.title = value ?? "";
},
};
#unmount?: () => void;
#template = () => (
<article>
{/* Named slot β€” consumer fills with slot="header" */}
<header>
<slot name="header" />
</header>
<main>
{/* Default slot β€” consumer children with no slot attribute */}
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</article>
);
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [cardSheet];
this.#unmount = render(shadow, this.#template);
}
disconnectedCallback() {
this.#unmount?.();
this.#unmount = undefined;
}
}

ElementsKit Slot β€” Light DOM

Without Shadow DOM, the browser does not project children. ElementsKit’s Slot primitive fills this gap: a pair of comment markers that reserve a region in the DOM. Content between them can be replaced reactively, with no wrapper element.

import { Slot } from "elements-kit/slot";
const slot = new Slot();
// Mounts the comment markers + optional default content
const section = <section>{slot.render("Loading…")}</section>;
// Later β€” replace content in place
slot.set(<p>Content loaded!</p>);
slot.isMounted(); // true
slot.parent(); // the <section> element

When do I need a Slot?

Component typeSlot needed?How
Function componentNoUse {props.children} / {props.foo} directly. The JSX runtime creates an internal Slot for any function-typed child and wires reactive replacement automatically.
Plain class component (no extends HTMLElement)OptionalDirect Slot fields if you want imperative control (this.body.set(node)). For pure JSX flow, function components are simpler.
Custom element (extends HTMLElement)Yes[SLOTS] = { ... } as const for collision-safe named slots. Plain instance fields collide with the inherited HTMLElement surface (title, children, slot, …).

Function component β€” no Slot at all

function Card(props) {
return (
<article>
<header>{props.header}</header>
<main>{props.children}</main>
<footer>{props.actions}</footer>
</article>
);
}
<Card
header={<h2>Title</h2>}
actions={<button>Confirm</button>}
>
Body content
</Card>

{props.children} and any function-typed prop flows through mountChild, which creates a Slot internally and updates in place when the source signal changes. The component doesn’t see the slot β€” it just renders the prop.

Custom element β€” [SLOTS] for named slots

import { Slot, SLOTS } from "elements-kit/slot";
import { attributes } from "elements-kit/attributes";
@attributes
class XCard extends HTMLElement {
// Named slots β€” keys flow into JSX as `slot:header` / `slot:footer`.
// The `[SLOTS]` symbol avoids collisions with HTMLElement properties.
[SLOTS] = { header: new Slot(), footer: new Slot() } as const;
// Default slot β€” surfaces as the JSX `children` prop.
children = new Slot();
connectedCallback() {
this.replaceChildren(
<article>
<header>{this[SLOTS].header.render("Untitled")}</header>
<main>{this.children.render()}</main>
<footer>{this[SLOTS].footer.render()}</footer>
</article>,
);
}
}
customElements.define("x-card", XCard);

Consumer JSX β€” fill slots via slot:name props:

<x-card
slot:header={<h2>My Title</h2>}
slot:footer={<button>Confirm</button>}
>
<p>Main body content.</p>
</x-card>

The JSX runtime reads slot:header and calls slot.set(<h2>My Title</h2>) on the matching Slot instance.

Reactive slot content

Pass a signal or () => T as slot content β€” the region updates in place when it changes. Works identically for function components and custom elements:

const title = signal("Initial Title");
<Card header={() => <h2>{title}</h2>}>
Body content
</Card>
// Slot content updates reactively β€” no re-render of surrounding tree
title("Updated Title");

SlotProps type helper

Props<XCard> already infers slots from [SLOTS] β€” prefer that when you have a concrete class to point at. SlotProps<K> is the fallback for cases where you can’t derive from an instance: function components that explicitly want slot:name syntax, or hand-written prop interfaces.

import type { Child, SlotProps } from "elements-kit/jsx-runtime";
// Declare which slot names are accepted alongside your own props
interface CardProps extends SlotProps<"header" | "footer"> {
title?: string;
children?: Child;
}
// TypeScript now knows slot:header and slot:footer are valid
<Card title="Hello" slot:header={<h1>Hi</h1>} />

Comparison

Native <slot>ElementsKit Slot
Shadow DOM requiredYesNo
Style encapsulationYesNo (global CSS)
Browser-native projectionYesNo (comment markers)
Reactive content updatesRequires JS re-renderYes β€” slot.set()
No wrapper elementYesYes
Named slotsYes (name attribute)Yes ([SLOTS] = { ... })
TypeScript slot:name propVia IntrinsicElementsVia SlotProps<K>

Choose native <slot> when you need style encapsulation or are building a reusable web component for external consumers. Choose ElementsKit Slot when you want reactive content swapping in a light DOM component without Shadow DOM overhead.


See also