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.

<template> (dormant)
styles: not applied
scripts: not run
media: not loaded
out of the document
clone →
insert
in the live DOM
styles: applied
scripts: run
media: plays
interactive
A template holds parsed-but-dormant HTML. Cloning and inserting it is what switches it on.

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.

inside <div>
<tr><td>Espresso</td></tr>
↓ parser “fixes” it
Espresso
tags stripped
inside <template>
<tr><td>Espresso</td></tr>
↓ kept as-is
<tr><td>Espresso</td></tr>
structure intact
Same markup, two containers. The div's contents get 'fixed'; the template's are preserved.

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:

interactivediv vs template: who keeps your markup

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.

DocumentFragment
<style>…</style>
<div class=“notice”>…</div>
append →
parent in DOM
<style>…</style>
<div class=“notice”>…</div>
no fragment wrapper
Inserting a DocumentFragment splices in its children; the fragment wrapper itself disappears.

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:

interactiveStamp rows from one template

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>
1. click
handler fires on #elem
2. attachShadow
create #shadow-root
3. clone + append
<style> and <p> go in
4. set text
“Sealed inside the shadow root!”
Click flow: attach a shadow root, clone the template into it, then fill in text.

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:

interactiveCloning a template into a shadow root

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, a DocumentFragment. Clone it with cloneNode(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.