The Template Element
What is the template element
Section titled “What is 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.
Accessing and cloning
Section titled “Accessing and cloning”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.
Why templates are efficient
Section titled “Why templates are efficient”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.
Runnable demo
Section titled “Runnable demo”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.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
<template> + cloneNode(true) | Parses the markup only once, then clones are fast and safe from XSS since there’s no repeated string parsing | Requires extra code to grab and clone the content — not as short as concatenating a string directly |
Building markup with an innerHTML string repeatedly | Easy to write, quick to see results for a prototype | Slower, since the HTML has to be re-parsed every time, and risks XSS if user data ends up in the string |
Common mistakes
Section titled “Common mistakes”- Forgetting to call
.cloneNode(true)before using a template’s content —template.contentis a single sharedDocumentFragment. Inserting it directly without cloning moves the original fragment out, leaving the next instance with an empty fragment. - Calling
cloneNode()withouttrue— this produces a shallow clone with no descendant nodes at all. Always passtruefor 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>withcloneNode(true)in components that need to stamp the same structure repeatedly, such as list items or menu options.