skip to content

A custom element <my-card> renders <slot></slot> in its shadow root, and the page writes <my-card><span>Hi</span></my-card>. After slotting, where does that <span> live in the DOM, and which of document.querySelector, host.querySelector and shadowRoot.querySelector find it?

level: middleimportance: should knowfreq 40%

answer

  1. projection, not relocation
  2. the caller keeps ownership
  3. parentNode is still the host
  4. shadow queries cannot see it
  5. assignedSlot and assignedNodes bridge the two

basics

~20 s

The <span> never moves. It stays a child of the host in the ordinary document tree, so document.querySelector and host.querySelector find it, while shadowRoot.querySelector does not. Slotting only projects it into the flattened tree the browser renders.

solid answer

~40 s

Slotting is a rendering projection, not a move. The `<span>` remains a light-DOM child of `<my-card>`: `span.parentNode` is the host, `document.querySelector('my-card span')` finds it, and `host.querySelector('span')` finds it. `shadowRoot.querySelector('span')` returns `null`, because the shadow tree contains a `<slot>` element and nothing else — the assigned nodes are not its descendants. The relation is exposed both ways: `span.assignedSlot` gives the slot it landed in, and `slot.assignedNodes()` lists what the slot received. What the browser renders is the **flattened tree**, in which each slot is substituted by its assigned nodes; that tree exists for rendering, not for DOM traversal. Practical consequence: a component reads caller content through its own light DOM (`this.querySelector`) or through the slot's assigned-nodes API, never through the shadow root.

code

javascript · 8 lines
javascript
const host = document.querySelector('my-card');
const slot = host.shadowRoot.querySelector('slot');
const span = host.querySelector('span');

console.log(span.parentNode === host);        // true  - never moved
console.log(host.shadowRoot.querySelector('span')); // null - not a shadow descendant
console.log(span.assignedSlot === slot);      // true  - the projection relation
console.log(slot.assignedElements());         // [ <span> ]

go deeper

for a junior

Remember the one-line rule: slotted content stays where the page put it. Ordinary document queries still find it; the component's shadowRoot does not contain it.

for a middle

Explain the three trees — light, shadow, flattened — and which APIs read which. Name assignedSlot on the node and assignedNodes/assignedElements on the slot as the bridge between ownership and rendering.

for a senior

Draw the practical conclusions: read caller content through the light DOM or the assignment API, never cache it, and expect page stylesheets to match slotted nodes because they are the page's nodes.

for a principal

Own the boundary question: slots give placement without ownership, so a component cannot validate or transform what it is handed. Decide when that is the right contract and when structured data on a property serves the library better.

## Three trees, one of which is not a DOM tree When a host element has a shadow root and slotted content, three structures are in play: 1. **The document tree (light DOM).** `<my-card>` with its `<span>` child, exactly as the page author wrote it. This is the tree every ordinary DOM API walks. 2. **The shadow tree.** What the component built: wrappers, a `<style>`, and `<slot>` elements. Its root is the `ShadowRoot`. 3. **The flattened tree.** A composed view in which every `<slot>` is replaced by the nodes assigned to it. This is what the browser lays out and paints — and it is not something you traverse with `querySelector`. Slotting affects only the third. No node is reparented, no node changes owner document. ## What each query sees ```js const host = document.querySelector('my-card'); host.querySelector('span'); // the <span> (light DOM child) document.querySelector('my-card span');// the <span> host.shadowRoot.querySelector('span'); // null host.shadowRoot.querySelector('slot'); // the <slot> span.parentNode === host; // true span.parentNode === slot; // false ``` The mirror-image rule matters just as much: `document.querySelector` cannot reach *into* the shadow tree. The two directions together are the whole model — the caller's content stays public and reachable; the component's own markup stays private behind `shadowRoot`. ## The assignment relation, from both ends The platform exposes the projection explicitly rather than making you infer it: ```js const slot = host.shadowRoot.querySelector('slot'); slot.assignedNodes(); // [ #text?, <span>, ... ] nodes, text included slot.assignedElements(); // [ <span> ] elements only slot.assignedNodes({ flatten: true }); // fallback content if nothing is assigned, // and chained slots resolved through span.assignedSlot; // the <slot> element, or null when unassigned ``` `assignedElements()` is usually what component code wants, because `assignedNodes()` includes the whitespace text nodes produced by formatted markup. `{ flatten: true }` answers a different question: "what will actually render here", which is why it substitutes fallback content for an empty slot and resolves a slot that has itself been assigned to another slot. ## Consequences that show up in real code **Reading caller content.** A component inspects what it was given through its own light DOM — `this.querySelectorAll('my-tab')` — or through `slot.assignedElements()`. Reaching through the shadow root returns nothing and is the single most common confusion here. **Ownership of behaviour.** Because slotted nodes are the page's nodes, the page can also mutate them at any time, and it does not need the component's permission. A component that caches a reference to a slotted child is caching something someone else owns; the assignment API plus the slot's `slotchange` event exist precisely so it can re-read instead. **Styling.** The nodes are in the document tree, so document stylesheets match them normally — a page rule `my-card span { color: red }` applies. The component's ability to style them from inside the shadow tree is deliberately narrower, and is a topic in its own right. **Events.** A `click` on the slotted `<span>` starts on a node in the light DOM, but propagation follows the flattened tree, so the path runs through the slot and the shadow root before continuing up the host's ancestors. The details of what `event.target` looks like from outside the boundary belong to shadow-DOM encapsulation; the point for slotting is only that the path is the composed one, not the plain light-DOM one. ## How to say it in an interview A compact, correct formulation: "Slotted content is the caller's content and stays in the caller's tree. The component gets to decide *where* it renders, not *who owns it*. `querySelector` follows ownership; rendering follows the flattened tree; `assignedNodes` and `assignedSlot` are how you cross between the two." That framing also explains the API's limits — a component cannot rewrite or reorder what it is handed the way a render-time library can clone and transform children. It can only route it, provide fallbacks, and react when the assignment changes.

  • What is the difference between slot.assignedNodes() and slot.assignedNodes({ flatten: true })?
    Plain `assignedNodes()` returns exactly the nodes assigned to that slot and an empty array when nothing is assigned. With `{ flatten: true }` you get what will actually render there: the slot's fallback children when the slot is empty, and the resolved nodes when a slot has itself been assigned to another slot in a nested component.
  • Why does slot.assignedNodes() often contain text nodes you did not expect?
    Formatted markup leaves newline-and-indentation text nodes as direct children of the host, and text nodes are slottable. `assignedNodes()` reports them faithfully. Use `assignedElements()` when you want only elements, or filter for non-whitespace text if the component genuinely accepts bare text.
  • A component caches slot.assignedElements() in its constructor and never re-reads it. What goes wrong?
    The caller owns those nodes and can add, remove or re-slot them at any time, so the cache silently goes stale — the component renders against content that is no longer assigned. Re-read the assignment when the slot's slotchange event fires instead of holding a snapshot.

saying these in an interview costs you the question

  • Says the browser moves slotted nodes into the shadow tree
  • Tries to find slotted content via shadowRoot.querySelector
  • Thinks the slotted node's parentNode is the <slot>
  • Believes slotting hides content from document queries
  • Confuses the flattened tree with a traversable DOM tree

context