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";
@attributesclass 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 = Slot.new();
// Mounts the comment markers + optional default contentconst section = <section>{slot("Loadingβ¦")}</section>;
// Later β replace content in placeslot.set(<p>Content loaded!</p>);
slot.isMounted(); // trueslot.parent(); // the <section> elementNamed slots with Slots<K>
For components with multiple named regions, use Slots<K> β a typed keyed collection of Slot instances. Attach it to your component instance via the SLOTS symbol so JSXβs slot:name prop wiring works automatically.
import { Slot, Slots, SLOTS } from "elements-kit/slot";import type { Child, Props } from "elements-kit/jsx-runtime";
/** * Props for `<CardComponent>` β derived from its public instance fields * (via `Props<>`) and the `[SLOTS]` declaration. */export type CardProps = Props<CardComponent>;
class CardComponent { // Phantom signature: JSX reads it to infer accepted props. Runtime // constructs with no args β createElement assigns each prop via // property set afterwards. constructor(_props?: CardProps) {}
// Named slots β keys flow into `CardProps` as `slot:header` / `slot:footer`. [SLOTS] = Slots.new(["header", "footer"] as const);
// Default slot β CardProps picks this up as a typed `children` key. children: Child = Slot.new();;
render() { return ( <article> <header> {/* Mount slot β "Default header" renders until filled */} {this[SLOTS].header("Default header")} </header> <main>{this.children()}</main> <footer>{this[SLOTS].footer()}</footer> </article> ); }}Consumer JSX β fill slots via slot:name props:
<CardComponent slot:header={<h2>My Title</h2>} slot:footer={<button>Confirm</button>}> <p>Main body content.</p></CardComponent>ElementsKitβs JSX runtime reads slot:header and calls slot.set(<h2>My Title</h2>) on the matching SlotInstance.
Reactive slot content
Pass a signal or () => T as slot content β the region updates in place when it changes:
const title = signal("Initial Title");
<CardComponent slot:header={() => <h2>{title}</h2>}> Body content</CardComponent>
// Slot content updates reactively β no re-render of surrounding treetitle("Updated Title");SlotProps type helper
Props<CardComponent> 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, components that donβt declare [SLOTS], or hand-written prop interfaces.
import type { Child, SlotProps } from "elements-kit/jsx-runtime";
// Declare which slot names are accepted alongside your own propsinterface CardProps extends SlotProps<"header" | "footer"> { title?: string; children?: Child;}
// TypeScript now knows slot:header and slot:footer are valid<CardComponent title="Hello" slot:header={<h1>Hi</h1>} />Comparison
Native <slot> | ElementsKit Slot | |
|---|---|---|
| Shadow DOM required | Yes | No |
| Style encapsulation | Yes | No (global CSS) |
| Browser-native projection | Yes | No (comment markers) |
| Reactive content updates | Requires JS re-render | Yes β slot.set() |
| No wrapper element | Yes | Yes |
| Named slots | Yes (name attribute) | Yes (Slots<K>) |
TypeScript slot:name prop | Via IntrinsicElements | Via 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.