# 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.

```tsx
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:

```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):

```tsx
<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:

```tsx
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.

```tsx
import { Slot } from "elements-kit/slot";

const slot = new Slot();

// Mounts the comment markers + optional default content
const section = <section>{slot.get("Loading…")}</section>;

// Later — replace content in place (native `append()` content: Node/string/array)
slot.set(<p>Content loaded!</p>);

slot.isMounted(); // true
slot.parent();    // the <section> element
```

### When do I need a `Slot`?

The rule is about **who fills the slot**, not the component kind. Anything
consumed through elements-kit JSX needs nothing special.

| Filled by                                                                             | Slot needed?    | How                                                                                                                                                                                                        |
| ------------------------------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **elements-kit JSX** (function, class, or custom element)                             | No              | Slots are plain properties — pass them as props, place `{this.foo}` / `{props.foo}` in the template. elements-kit JSX assigns each prop; your render places it. Use `@reactive` to update on reassignment. |
| **Imperative DOM code** (a vanilla script holding a `Node` and doing `el.foo = node`) | Yes (`@slot()`) | `@slot()` makes the property a live comment-marker region so imperative code can fill and *replace* content against the mounted DOM — no reactive context required.                                        |

Other JSX frameworks (React, Vue, Svelte) each have their own children model
and don't hand DOM nodes to props, so they seldom fill slots directly. When
they must, it's through their imperative escape hatch (a `ref` to the element,
then `el.foo = node`) — the same path as vanilla, which is what `@slot()`
serves.

#### Function component — no `Slot` at all

```tsx
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 imperative consumers — `@slot()`

If your custom element is consumed through elements-kit JSX, you don't need
`@slot()` — declare plain (or `@reactive`) properties and place them in your
render, exactly like the class-component case above. Reach for `@slot()` only
when **imperative DOM code** (a vanilla script, or another framework's
`ref`-then-assign escape hatch) must fill and replace slots against the
mounted element.

Here the element is authored with **no elements-kit rendering at all** — plain
DOM in `connectedCallback`. Reading a `@slot()` property returns its live
region; placing it with `append()` mounts the markers:

```ts
import { slot, type SlotContent } from "elements-kit/slot";

class XCard extends HTMLElement {
  @slot() header!: SlotContent; // named slot
  @slot() body!: SlotContent;   // named slot

  connectedCallback() {
    const article = document.createElement("article");
    const head = document.createElement("header");
    const main = document.createElement("main");
    head.append(this.header); // reading the region places it — markers mount here
    main.append(this.body);
    article.append(head, main);
    this.replaceChildren(article);
  }
}

customElements.define("x-card", XCard);
```

Now imperative code can fill and *replace* a slot at any time — the swap lands
directly in the live DOM, after mount, with no reactive context:

```ts
const el = document.querySelector("x-card")!;
el.header = document.createElement("h2"); // Node
el.header = "plain text";                 // native append() content
el.header = "updated";                    // replaces the previous content in place
el.header = null;                         // clears
```

That post-mount live swap is the *only* thing `@slot()` buys you. Consume the
same element through elements-kit JSX (`<x-card header={<h2/>}>`) and you
wouldn't need `@slot()` at all — a plain (or `@reactive`) field works, because
elements-kit JSX assigns props *before* the element mounts, so the element's
own render reads the final value directly.

### 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:

```tsx
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");
```

***

## 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 (`@slot()` properties)                       |
| TypeScript named slot prop | Via `IntrinsicElements` | The property's declared type (`PropertiesOf<C>`) |

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

* [Custom elements](/custom-elements)
* [Elements](/elements)
* [Components](/components)
