template and slot
HTML template elements for inert DOM clones, slot projection in shadow DOM, and when to prefer templates over innerHTML strings.
- javascript
- template-and
Two different “template” ideas collide in frontend interviews: the HTML <template> element (inert document fragment factory) and <slot> (content projection in shadow DOM). Both help structure UI without painting hidden junk to the screen.
<template> — parse once, clone many
<template id="row-tpl">
<tr>
<td class="name"></td>
<td class="score"></td>
</tr>
</template>
const tpl = document.getElementById('row-tpl');
function addRow(name, score) {
const node = tpl.content.cloneNode(true);
node.querySelector('.name').textContent = name;
node.querySelector('.score').textContent = score;
document.querySelector('tbody').appendChild(node);
}
Content inside <template> is not rendered and not active (scripts don’t run, images don’t load) until cloned into a live tree. Better than innerHTML loops for repeated structure — especially with event wiring on clones.
// DocumentFragment under the hood
tpl.content; // DocumentFragment
Why not only strings?
// XSS if name is untrusted
tr.innerHTML = `<td>${name}</td>`;
// textContent assignment after clone is safer by default
Templates still require care if you innerHTML into the clone. Prefer textContent / setAttribute with known tokens.
<slot> — projection
Inside a shadow root, slots mark placeholders for the host’s children:
this.attachShadow({ mode: 'open' }).innerHTML = `
<header><slot name="title">Default title</slot></header>
<main><slot></slot></main>
`;
<my-card>
<span slot="title">Invoice</span>
<p>Body copy lives in light DOM</p>
</my-card>
- Named slots match
slot="title". - Default slot catches the rest.
- Fallback content in the shadow shows when nothing is provided.
- Light DOM remains in the host for a11y trees (with nuances).
const slot = shadow.querySelector('slot');
slot.addEventListener('slotchange', () => {
const assigned = slot.assignedNodes({ flatten: true });
});
Together in a component
class UserBadge extends HTMLElement {
static template = document.createElement('template');
static {
UserBadge.template.innerHTML = `
<style>:host{display:inline-flex}</style>
<img part="avatar" alt="" />
<slot></slot>
`;
}
constructor() {
super();
this.attachShadow({ mode: 'open' })
.appendChild(UserBadge.template.content.cloneNode(true));
}
}
One parsed template, many instances — classic web component pattern.
Interview answer (out loud)
“HTML template holds inert DOM you clone with cloneNode for repeated UI without rendering hidden content. Slots in shadow DOM project host children into the shadow tree, with named and default slots. Prefer textContent when filling clones to avoid XSS from string HTML.”
Cloning performance
Parse cost of a large HTML string via innerHTML repeatedly is worse than cloning a pre-parsed template. For tables with thousands of rows, still prefer:
- Template clone for structure
- Or virtualization (don’t create all rows)
DocumentFragmentto batch appends
const frag = document.createDocumentFragment();
for (const row of rows) {
frag.appendChild(makeRow(row)); // makeRow uses template
}
tbody.appendChild(frag);
Named slot fallback UX
<slot name="actions">
<button type="button" disabled>No actions</button>
</slot>
Fallback lives in shadow DOM and disappears when the user projects content into that slot — good progressive UI for empty states inside components.
Styling slotted content
::slotted(span) { color: blue; } /* limited — element only, not deep */
You cannot deep-style arbitrary trees inside slotted content from the shadow easily — by design. Expose CSS variables on the host instead.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.