ESC

Type to search the knowledge base.

template and slot

HTML template elements for inert DOM clones, slot projection in shadow DOM, and when to prefer templates over innerHTML strings.

intermediate3 min read
  • 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:

  1. Template clone for structure
  2. Or virtualization (don’t create all rows)
  3. DocumentFragment to 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 guides