skip to content

In a Web Component's shadow DOM, how does a <slot> decide which of the host element's children it renders, and what happens to a child whose slot attribute matches no slot?

level: middleimportance: must knowfreq 52%

answer

  1. direct children only, never grandchildren
  2. exact name match, both sides
  3. no match means nothing renders
  4. slot children are the fallback
  5. first matching slot in tree order

basics

~20 s

A <slot> without a name takes the host's direct children that have no slot attribute; <slot name="x"> takes the direct children whose slot="x" matches exactly. A child matching no slot is never rendered, silently. An empty slot renders its own children as fallback.

solid answer

~50 s

Assignment is a name match between the host's **direct children** and the slots in its shadow tree. A child with `slot="title"` goes to the first `<slot name="title">` in tree order; a child with no `slot` attribute goes to the default slot, the one declared as plain `<slot>`. Deeper descendants are not slottable — only the host's own children are, so you cannot slot a grandchild. Text nodes count as slottable, which is why stray whitespace can make a default slot non-empty. If nothing matches a child — a typo, or no default slot exists — that child is simply not rendered, with no warning; it is still in the DOM, just absent from the flattened tree. Conversely, a slot with nothing assigned renders its own children as fallback content, which disappears the moment anything is assigned.

code

html · 16 lines
html
<script>
customElements.define('my-card', class extends HTMLElement {
  connectedCallback() {
    if (this.shadowRoot) return;
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <header><slot name="title">Untitled</slot></header>
      <section><slot></slot></section>`;
  }
});
</script>

<my-card>
  <h2 slot="title">Invoice</h2>
  <p>Body text goes to the default slot.</p>
  <p slot="foot">No matching slot, so this never renders.</p>
</my-card>

go deeper

for a junior

Know the two halves of the contract: slot="name" on the caller's child, name="name" on the <slot>, and a plain <slot> for everything unnamed. Say plainly that unmatched content does not render.

for a middle

Explain that only direct children are slottable, that the first matching slot in tree order wins, and that a slot's own children are fallback shown only while nothing is assigned.

for a senior

Be ready to debug the silent failure: content present in the DOM but absent from the render because assignedSlot is null. Mention the whitespace-text-node trap that defeats fallback content.

for a principal

Own the API-design view: slot names are the public contract between component and caller, so the count and naming of slots is a compatibility surface you version, not an implementation detail.

## The two trees, and what slotting actually does A custom element with a shadow root has two sets of children: - its **light DOM** children — what the page author wrote between the tags; - its **shadow tree** — what the component itself created inside `attachShadow`. By default the light children are not rendered at all: the shadow tree replaces them. A `<slot>` is the component saying "render some of the caller's children *here*". The browser builds a third, virtual view — the **flattened tree** — in which each slot is replaced by the nodes assigned to it, and that flattened tree is what gets rendered. Nothing moves. Slotting is a projection, not a relocation. ## The matching rules 1. **Only direct children of the host are slottable.** Elements *and* text nodes. A grandchild can never be assigned; it renders as part of its slotted ancestor or not at all. 2. **`slot="name"` on the child, `name="name"` on the slot.** The match is an exact string comparison. 3. **No `slot` attribute means the default slot** — a `<slot>` element with no `name` (equivalently `name=""`). 4. **First match in tree order wins.** Two `<slot name="footer">` elements in one shadow root: the first one receives everything, the second stays empty and shows its fallback. 5. **Several children may share one slot.** They render in document order, not in the order they were assigned. 6. **No match means no render.** No error, no console warning. ```html <my-card> <h2 slot="title">Invoice</h2> <!-- -> <slot name="title"> --> <p>Body text</p> <!-- -> <slot> (default) --> <p slot="foot">Total</p> <!-- no <slot name="foot">: invisible --> </my-card> ``` ## Fallback content The children of a `<slot>` element are its fallback: rendered only while the slot has no assigned nodes. ```html <slot name="icon"><svg class="default-icon"></svg></slot> ``` Assign anything to that slot and the default `<svg>` stops rendering — it is not merged with, and not appended to, the caller's content. Fallback is all-or-nothing per slot, which is why components that want "caller content *plus* a default decoration" use two slots rather than one slot with fallback. A subtlety worth knowing: whitespace text nodes are slottable. Formatted markup like ```html <my-card> <h2 slot="title">Invoice</h2> </my-card> ``` assigns the newline-and-indentation text nodes around the `<h2>` to the default slot. The default slot is therefore *not* empty and its fallback content will not show. Components that depend on fallback usually guard with `:host` styling or check `slot.assignedNodes({ flatten: true })` for meaningful content rather than trusting emptiness. ## The silent-failure trap The most common bug in a slot-based API is a typo in the slot name. Because unmatched children are simply absent from the flattened tree, the symptom is missing UI with a perfectly healthy DOM: `document.querySelector('[slot="foot"]')` finds the element, DevTools shows it under the host, it is just not painted. Two habits contain this: always declare a default `<slot>` so unnamed and misrouted content has somewhere to go if that suits your design, and in development log any host child whose `assignedSlot` is `null`. ```js for (const child of host.children) { if (!child.assignedSlot) console.warn('unslotted:', child); } ``` `Element.assignedSlot` is the per-node view of the same relation that `slot.assignedNodes()` gives from the slot's side. ## Why the API is shaped this way Slots exist so a component can own **structure** while the caller owns **content**. The component decides that a card has a header region, a body region, and a footer region, and how they are laid out and labelled; the caller decides what goes in each. That is the same inversion of control as a `children` prop in a component framework, but resolved by the browser at render time rather than by a library at call time — which is why the component cannot inspect or transform what it is given, only decide where it lands.

  • A component declares <slot name="icon"><svg/></slot> and the caller supplies an icon. Does the caller's icon render alongside the default svg?
    No. Fallback content renders only while the slot has no assigned nodes; the moment anything is assigned, the entire fallback stops rendering. It is a replacement, not a merge. If you want caller content plus a permanent decoration, render the decoration outside the slot or give it its own slot.
  • Why can a component not slot a grandchild, for example the <li> elements inside a caller's <ul>?
    Only the host's direct children are slottable, so the `<ul>` is assignable but its `<li>` children are not. They render as part of the slotted `<ul>` subtree. If a component needs to place individual items, its API must ask the caller to write them as direct children of the host.
  • The default slot's fallback content never appears, even when the caller writes no children. What is the likely cause?
    Whitespace. Formatted markup leaves newline-and-indentation text nodes as direct children of the host, and text nodes are slottable, so the default slot has assigned nodes and suppresses its fallback. Check with `slot.assignedNodes({ flatten: true })` and filter for non-whitespace, rather than assuming emptiness.

saying these in an interview costs you the question

  • Thinks any descendant can carry a slot attribute
  • Expects a console error when a slot name does not match
  • Believes fallback content merges with assigned content
  • Says slotted nodes are moved into the shadow tree
  • Assumes duplicate slot names split the assigned nodes

context