skip to content

Templates and Slots

You will learn how inert template markup and slot-based composition let a component accept caller-supplied content. Interviewers connect this directly to props.children and Vue slots to see whether you grasp the shared idea.

on this pageshow

questions

5

In HTML, what does the <template> element do with the markup written inside it, and how do you get a copy of that markup onto the page?

level: juniorimportance: must knowfreq 58%

answer

  1. parsed, but never in the document
  2. the element itself looks empty
  3. content is a DocumentFragment
  4. clone before you insert
  5. appending the fragment empties it

basics

~20 s

The <template> element parses its markup into an inert DocumentFragment exposed as template.content: nothing renders, scripts do not run, images are not fetched. To use it, clone that fragment with cloneNode(true) and insert the copy.

solid answer

~40 s

`<template>` is parsed by the HTML parser like any other markup, but its children are not put into the document tree — they are moved into a `DocumentFragment` on the element's `content` property, owned by a separate inert document. Because those nodes are not in the rendered document, they are not displayed, `<script>` inside does not execute, `<img src>` is not fetched, and custom elements inside are not upgraded. To render it you take a copy: `const frag = tpl.content.cloneNode(true)` (or `document.importNode(tpl.content, true)`), fill in the copy, then `parent.appendChild(frag)`. Two traps: appending `tpl.content` itself *moves* the nodes out, leaving the template empty for the next use, so always clone; and `tpl.querySelector(...)` finds nothing, because the template element has no children of its own — you must query `tpl.content`.

go deeper

for a junior

Be able to say that the markup inside <template> is parsed but not rendered, that it lives on the .content DocumentFragment, and that you clone that fragment with cloneNode(true) before inserting it.

for a middle

Explain why the contents are inert — a separate owner document, not CSS — and why appending .content directly empties the template. Mention filling the clone before insertion so the browser lays out once.

for a senior

Show judgment on when a cloned template beats an HTML string: no injection sink, parse errors surface at author time, and repeated stamping is cheap. Be ready to discuss where it stops being enough and a rendering library takes over.

for a principal

Frame it as a primitive, not a solution: <template> gives cheap, safe, static stamping with zero binding. Own the call about where a codebase draws the line between platform stamping and adopting a rendering layer.

## The problem <template> solves Before `<template>`, reusable markup was kept either in a hidden container (`<div style="display:none">`) or in a string — often inside `<script type="text/x-template">`. A hidden container is a bad hiding place: it is still in the document, so its images are fetched, its scripts run, its ids collide with real ids, and a query like `document.querySelectorAll('.row')` picks up the fake rows. A string is worse: no parsing until you concatenate it, and the usual `innerHTML` injection risk. `<template>` gives markup a place to sit where the parser has already validated and parsed it, but the document has not adopted it. ## What "inert" concretely means When the parser meets `<template>`, it parses the children normally but appends them to the element's **template contents**: a `DocumentFragment` reachable as `template.content`, whose node document is a separate, browserless "template contents owner" document. Consequences you can state in an interview: - Nothing inside is rendered — and not because of CSS. There is no box to style; `display` is irrelevant. - `<script>` inside does not execute while it sits there. - Resource-loading attributes do not load: no `<img src>` fetch, no `<video>` preload. - Custom elements inside are not upgraded, so no lifecycle callbacks run. - The markup is still *parsed*, so a typo is fixed up by the parser now, not at insertion time. ```html <template id="row"> <li><img src="/avatar.png" alt=""></li> </template> <!-- no network request for /avatar.png happens for this markup --> ``` ## Where the nodes actually live This is the detail that trips people up: the template element itself has **no children**. ```js const tpl = document.getElementById('row'); tpl.childNodes.length; // 0 tpl.querySelector('li'); // null tpl.content.childNodes.length; // the parsed nodes are here tpl.content.querySelector('li'); // the <li> ``` `innerHTML` is the exception: the HTML standard redirects a template's `innerHTML` getter and setter to the template contents, so `tpl.innerHTML` does show the markup, and assigning to it replaces the fragment's children. Everything else — `children`, `querySelector`, `textContent` — sees an empty element. ## Cloning, and the move-versus-copy trap A `DocumentFragment` is a container that disappears when inserted: `appendChild(frag)` moves the fragment's children into the target and leaves the fragment empty. That is exactly what you want for a freshly cloned copy, and exactly what you do *not* want for `template.content` itself: ```js list.appendChild(tpl.content); // WRONG: template is now empty list.appendChild(tpl.content.cloneNode(true)); // right: template stays reusable ``` `cloneNode(true)` performs a deep clone. `document.importNode(tpl.content, true)` does the same thing and additionally sets the copy's owner document to the current document; in practice `appendChild` adopts the nodes anyway, so both forms work and `cloneNode(true)` is the common idiom. Fill in the clone *before* inserting it, so the browser does one style/layout pass for the finished subtree rather than one per mutation: ```js const frag = tpl.content.cloneNode(true); frag.querySelector('.name').textContent = user.name; list.appendChild(frag); ``` One consequence of cloning: a `<script>` that never started inside the template is a fresh, un-started script in the clone, so inserting the clone into the document *does* run it. That differs from `innerHTML`, where injected `<script>` elements never execute. ## Why it matters beyond web components `<template>` is usually introduced alongside custom elements — a component's shadow markup is often written as a template and cloned into the shadow root on construction — but it stands alone. Any list rendering, any "insert this row again" UI, is a legitimate use, and it is the injection-safe alternative to building HTML strings: the structure is authored as markup, and only text goes in through `textContent`. Its limitation is that it is a *static* stamp. There is no binding, no interpolation, no re-render: you clone, you set values imperatively, you insert. That is why frameworks do not build on it, and why in an interview the honest framing is "the platform's primitive for inert, reusable markup", not "the platform's templating engine".

  • Why is hiding markup in a <div hidden> not equivalent to putting it in a <template>?
    `hidden` only removes the box from rendering. The nodes are still in the document, so their images are fetched, their scripts have already run, their ids duplicate real ids, and `document.querySelectorAll` matches them. A template's contents are in a separate inert document, so none of that happens — the browser has parsed them but the page has not adopted them.
  • You inserted a clone and then set textContent on the original template's nodes. Nothing changed on screen. Why?
    Cloning copies; it does not link. The inserted nodes are independent of `template.content`, so mutating the template affects only future clones. There is no binding between a template and the copies stamped from it — if you need to update rendered output, hold a reference to the inserted nodes.
  • Does a <script> inside a <template> ever run?
    Not while it sits in the template — the contents are inert. But cloning produces a script element that has not started, so inserting the clone into the document does execute it. That is unlike `innerHTML`, where injected `<script>` elements are inserted already flagged as started and never run.

saying these in an interview costs you the question

  • Says <template> is just display:none applied by the browser
  • Appends template.content directly, then finds the template empty
  • Uses tpl.querySelector instead of tpl.content.querySelector
  • Claims images and scripts inside a template still load
  • Calls <template> a templating engine with data binding

context

open as a page

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%

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.

open as a page

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%

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.

open as a page

In a Web Component, a shadow-root listener recomputes on a slot's slotchange event, but it never fires when the caller edits the text inside an element that is already slotted. Why not, and what does slotchange actually signal?

level: seniorimportance: should knowfreq 32%

basics

~20 s

slotchange signals that a slot's assigned node list changed — a slottable child added, removed, reordered, or re-routed by a slot attribute. It says nothing about mutations inside an already-assigned node, so editing that element's text or attributes fires nothing.

open as a page

You maintain a design-system library of custom elements. How do you decide what a component exposes as a named <slot> versus what it renders itself, and what does publishing a slot name commit you to?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Slot what the caller must own — arbitrary content whose markup you cannot predict; render yourself what the component is responsible for, such as structure, layout and accessibility wiring. A published slot name is public API: renaming one makes consumer content vanish silently.

open as a page