Skip to content

The Template Element

When you need to stamp out the same DOM structure over and over (like cards in a list), building elements with an innerHTML string every time is both slow and risks XSS — <template> solves this by letting you define markup once, parsed a single time, then cloned as many times as you need.

The <template> element holds HTML markup that is NOT rendered when the page loads. Unlike a hidden <div>, template content has no side-effects: scripts inside it do not execute, images do not load, and styles do not apply. Its content property exposes the markup as a DocumentFragment — a lightweight container of parsed nodes that lives outside the live document tree until you explicitly insert it.

Use document.querySelector('template').content (or getElementById) to get the DocumentFragment. Call content.cloneNode(true) to make a deep copy with all descendant nodes included, ready to insert anywhere in the document.

<template id="card-tpl">
<div class="card"><p class="msg"></p></div>
</template>
const tpl = document.getElementById('card-tpl');
const clone = tpl.content.cloneNode(true);
clone.querySelector('.msg').textContent = 'Hello from template!';
document.body.appendChild(clone);

Each call to cloneNode(true) produces an independent copy — mutating one clone does not affect the original DocumentFragment or any other clone.

Because template content lives in an inert DocumentFragment, the browser performs no layout, no styling, and no script execution for it until you clone and insert it. The parsing cost is paid once when the page loads, and every subsequent cloneNode(true) is a fast structural copy. This makes <template> ideal for stamping many instances of the same markup — for example, list items, cards, or repeated custom elements.

The custom element my-card below uses a <template> defined in the light DOM. In its connectedCallback it attaches a shadow root and appends a clone of the template’s content. Both instances share the same parsed template, demonstrating the “parse once, stamp many” pattern.

The <template> lives in the light DOM and is accessed by ID. Each my-card instance gets its own shadow root populated with an independent clone of the template content.

OptionBenefitCost
<template> + cloneNode(true)Parses the markup only once, then clones are fast and safe from XSS since there’s no repeated string parsingRequires extra code to grab and clone the content — not as short as concatenating a string directly
Building markup with an innerHTML string repeatedlyEasy to write, quick to see results for a prototypeSlower, since the HTML has to be re-parsed every time, and risks XSS if user data ends up in the string
  • Forgetting to call .cloneNode(true) before using a template’s content — template.content is a single shared DocumentFragment. Inserting it directly without cloning moves the original fragment out, leaving the next instance with an empty fragment.
  • Calling cloneNode() without true — this produces a shallow clone with no descendant nodes at all. Always pass true for a deep copy.
  • Expecting scripts or styles inside a <template> to run immediately on page load — template content is inert until it’s cloned and inserted into the real document; scripts won’t run and styles won’t apply until then.

💡 Real-world examples

Lit uses a similar internal concept to <template> to cache parsed markup and clone it on re-render, reducing repeated parsing cost.

Shoelace and other web component libraries commonly pair <template> with cloneNode(true) in components that need to stamp the same structure repeatedly, such as list items or menu options.

What does `template.content` return?
Why is `<template>` content not rendered by default?
What method creates a ready-to-insert copy of the template's content?