ESC

Type to search the knowledge base.

template Element

HTML template holds inert DOM clones — stamp client-side fragments, pair with slots/web components, and avoid XSS when filling them.

intermediate3 min read
  • html
  • template

The <template> element holds HTML that is parsed but not rendered and not active (scripts don’t run, images don’t load) until you clone its contents into a live document. It’s the platform’s lightweight client-side partial.

Docs: MDN <template>, DocumentFragment.

Basic stamp pattern

<template id="row-template">
  <tr>
    <td class="name"></td>
    <td class="amount"></td>
    <td><button type="button" class="remove">Remove</button></td>
  </tr>
</template>

<table>
  <tbody id="rows"></tbody>
</table>
const tpl = document.querySelector("#row-template");
const tbody = document.querySelector("#rows");

function addRow({ name, amount }) {
  const node = tpl.content.cloneNode(true);
  node.querySelector(".name").textContent = name;
  node.querySelector(".amount").textContent = amount;
  node.querySelector(".remove").addEventListener("click", (e) => {
    e.target.closest("tr")?.remove();
  });
  tbody.append(node);
}

Key points:

  • Use template.content (a DocumentFragment), not the template element itself.
  • cloneNode(true) every time — don’t move the original fragment nodes once.
  • Fill with textContent / safe APIs, not raw innerHTML with user strings.

Why not just a hidden div?

<template> Hidden <div>
Active resources Inert until stamped May load images/scripts
In document flow Not rendered Occupies accessibility considerations if not careful
Semantics Explicit inert fragment Easy to forget

Web components

Templates pair with Shadow DOM:

<template id="user-card">
  <style>
    :host {
      display: block;
      border: 1px solid #ddd;
      padding: 0.75rem;
    }
  </style>
  <h2 part="title"></h2>
  <slot></slot>
</template>
customElements.define(
  "user-card",
  class extends HTMLElement {
    connectedCallback() {
      if (this.shadowRoot) return;
      const root = this.attachShadow({ mode: "open" });
      root.append(document.querySelector("#user-card").content.cloneNode(true));
      root.querySelector("h2").textContent = this.getAttribute("name") ?? "";
    }
  }
);

Frameworks vs template

React/Vue/Svelte own templating. Native <template> still appears in:

  • Design-system docs with vanilla demos
  • Progressive enhancement without a bundler
  • Web components
  • Server-rendered HTML with small client sprinkles

Don’t mix competing renderers on the same subtree without clear ownership.

XSS

// Bad
node.querySelector(".name").innerHTML = user.name;

// Good
node.querySelector(".name").textContent = user.name;

Cloning a template doesn’t sanitize data you insert later.

Interview out-loud

“<template> stores inert HTML. I clone template.content, fill safe text, and append the fragment. It’s better than hidden live DOM for partials and pairs with web components. Frameworks replace many use cases, but the element remains useful for progressive enhancement.”

Footguns

  1. Appending the fragment once and wondering why the second stamp is empty — clone each time.
  2. Querying inside template without .content.
  3. Scripts inside template don’t run until moved — then may run; know the rules.
  4. Using template IDs that collide across partials.
  5. innerHTML with untrusted data after clone.

Event binding note

Cloned nodes do not copy event listeners from live prototypes unless you re-bind after clone (as in the stamp example). For many rows, use event delegation on tbody instead of per-row listeners:

tbody.addEventListener("click", (e) => {
  const btn = e.target.closest("button.remove");
  if (btn) btn.closest("tr")?.remove();
});

Templates then only need structure and text slots — behavior lives once on the parent.

Streaming HTML note

Some frameworks stream HTML with declarative shadow DOM and templates. Understand that template content is still inert until upgraded. Do not assume SEO crawlers execute your template stamping JS — critical content should exist as live HTML for public pages. Use templates for repeated client widgets, not for the only copy of primary content.

Further reading

Related guides