Template element
The built-in <template> element is a parking lot for HTML. You write markup inside it, the browser parses that markup and checks it for validity, but it does nothing with the result: no rendering, no styles, no scripts. The content sits there, inert, until your JavaScript reaches in, grabs it, and drops it somewhere on the page. At that moment it comes alive.
That “parse now, use later” split is the whole point. You could, in theory, hide markup in any element — stuff it in a <div style="display:none"> and read it back later. So why does <template> earn its own tag? Two reasons that no ordinary element can match, and both come down to how the browser treats the bytes inside.
insert
Why not just a hidden div
The first advantage is validation with freedom. The content of a <template> can be any syntactically correct HTML, including fragments that would normally be illegal anywhere else.
Take a lone table row. A <tr> is only meaningful inside a <table> (technically inside <thead>, <tbody>, or <tfoot>). Put one inside a <template> and it stays exactly as written:
<template>
<tr>
<td>Espresso</td>
</tr>
</template>
Try the same thing inside a <div> and the browser’s HTML parser steps in. It knows a <tr> cannot live loose in a <div>, decides your markup is broken, and quietly repairs it — often by throwing the <tr> and <td> tags away entirely, leaving you with just the text. A <template> suspends that repair logic. It checks that your HTML is well-formed, then keeps the tree intact, wrappers and all.
See it live. Type any HTML fragment below and hand the same string to a <div> and to a <template> via innerHTML, then look at what each one actually kept. Orphan table tags survive in the template but get stripped or rearranged in the div:
The second advantage is inertness. You can drop styles and scripts inside a <template> and nothing happens until you ask it to:
<template>
<style>
h2 { color: crimson; }
</style>
<script>
alert("Widget ready");
</script>
</template>
On page load that <style> rule does not affect any heading, and that alert never fires. The browser treats template content as living “out of the document” — a <video autoplay> in there does not start playing, an <img> does not eagerly fetch, an inline script does not execute. Everything is staged, waiting.
The switch flips when you insert the content into the live document. From that point the styles apply, the scripts run, and media behaves normally. That controlled activation is what makes <template> a clean building block for components: you can define a chunk of behavior once and turn it on precisely when and where you decide.
Inserting template content
The parsed markup does not live in the template’s childNodes the way you might expect. It lives in a separate property: template.content. And its type is special — it is a DocumentFragment, a lightweight DOM node designed to be a temporary container for a group of nodes.
A DocumentFragment behaves like a normal node in almost every way. You can query inside it, append to it, walk it. It has one defining trick: when you insert a fragment into the DOM, the fragment itself vanishes and its children take its place. You never end up with a DocumentFragment wrapper in your live tree — only its contents.
Here is the pattern in full. Notice the cloneNode(true) — that true means a deep clone, copying the fragment and everything nested inside it:
<template id="tmpl">
<script>
alert("Booting up");
</script>
<div class="notice">Welcome aboard!</div>
</template>
<script>
let elem = document.createElement('div');
// Deep-clone the template content so we can reuse it multiple times
elem.append(tmpl.content.cloneNode(true));
document.body.append(elem);
// Now the script from <template> runs
</script>
Also worth pointing out: tmpl is used as a bare variable here even though it is only an id on the element. Browsers expose elements with an id as global variables of the same name. It is handy for small examples, but in real code prefer document.getElementById('tmpl') — the implicit globals are fragile and easy to shadow.
This is where cloning pays off. One template, defined once, becomes the stamp for as many list rows as you like. Each click below deep-clones content, patches the text of the clone, and appends it — the original template stays full and ready for the next stamp:
Rebuilding a Shadow DOM example with template
Templates pair naturally with shadow DOM. In the shadow DOM chapter we filled a shadow root by hand; here the same result comes from cloning a template. The markup for the component — its private styles and structure — lives declaratively in the <template>, and the click handler just clones it in.
<template id="tmpl">
<style> p { color: rebeccapurple; } </style>
<p id="greeting"></p>
</template>
<div id="elem">Reveal card</div>
<script>
elem.onclick = function() {
elem.attachShadow({mode: 'open'});
elem.shadowRoot.append(tmpl.content.cloneNode(true)); // (*)
elem.shadowRoot.getElementById('greeting').innerHTML = "Sealed inside the shadow root!";
};
</script>
At line (*) we clone tmpl.content and append it to the shadow root. Because it is a DocumentFragment, its children — the <style> and the <p> — are what actually land inside. The fragment splices itself away.
The resulting shadow tree:
<div id="elem">
#shadow-root
<style> p { color: rebeccapurple; } </style>
<p id="greeting"></p>
</div>
Because the styles now live inside the shadow root, that p { color: rebeccapurple; } rule applies only to the paragraph in this component. It cannot leak out and recolor every other paragraph on the page. Template plus shadow DOM gives you both reusable markup and scoped styling.
Try it. The template below carries a bold, purple <style> rule right next to its paragraph. Clicking builds the component: a shadow root is attached and the template is cloned into it. Watch the plain paragraph outside the component — it stays untouched, proving the template’s styles never escaped the shadow boundary:
What template does not do
It is easy to expect too much from <template>. It is a storage-and-cloning primitive, nothing more. There is no {{ variable }} substitution, no for loop over a list, no data binding that keeps the DOM in sync with your state. Frameworks like Vue, Lit, or Angular add all of that; the native element does not.
What you do get is a fast, clean foundation to build those things on. Clone the fragment, then walk it with ordinary DOM methods — querySelector, textContent, setAttribute — to fill in the dynamic parts before you insert.
Summary
The <template> element is an inert container for HTML you plan to reuse:
- Its content can be any syntactically valid HTML, and the browser parses it without rendering it.
- That content is treated as living out of the document, so its styles do not apply, its scripts do not run, and its media does not load — until you insert it.
- You reach the parsed markup through
template.content, aDocumentFragment. Clone it withcloneNode(true)and insert the clone wherever you need it.
A few properties make it genuinely unique:
- The browser validates the HTML inside it, unlike raw strings sitting in a script.
- Yet it still tolerates top-level tags that make no sense on their own, like a bare
<tr>or<td>. - Inserting the content into the document is the trigger that brings it to life: scripts execute,
<video autoplay>plays, styles take effect.
There is no built-in iteration, data binding, or variable substitution. Those are yours to add on top — and cloning plus a handful of DOM writes is usually all it takes.