A native way to stamp out markup
Client-side templating has been a developer staple for years, long before the current wave of MVC frameworks made it ubiquitous. Server-side engines handled it first; then JavaScript libraries brought the pattern to the browser. But the web platform itself never had a standard, native mechanism — until the <template> element arrived in the WhatWG HTML specification.
The idea is simple: declare a fragment of markup, have the browser parse it as HTML, and leave it dormant until you explicitly activate it at runtime. The element's contents are inert chunks of cloneable DOM — scaffolding you can reuse throughout the life of an app.
Rafael Weinstein, a spec author, described it this way:
They're a place to put a big wad of HTML that you don't want the browser to mess with at all…for any reason.
How to detect and declare templates
Feature detection for <template> is straightforward: create the element and check for the existence of the .content property.
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
To define a template, wrap your markup in a <template> element. The contents are parsed but have none of the usual side effects until you use them.
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
What makes template content special
Wrapping markup in a <template> gives it four defining properties:
- It is inert until activated. The content is hidden DOM and doesn't render.
- It has no side effects. Scripts inside the template don't execute, images don't load, and audio doesn't play — until the template is stamped out.
- It's not in the document. Calls to
document.getElementById()orquerySelector()from the main page won't match nodes inside a template. - It can go almost anywhere. A
<template>is valid inside<head>,<body>, or<frameset>. Crucially, it also works in places where the HTML parser usually rejects content — for example, as a child of<table>or<select>.
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
Activating a template
A template only does something once you activate it. The simplest way is to deep-copy its .content — a read-only DocumentFragment — with document.importNode().
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
After stamping out the template, the cloned content goes live: image requests fire, scripts can run, and the markup renders.
Inert content and Shadow DOM
The inertness of template content is easy to demonstrate. In this example, a <script> inside a template runs only when a button is pressed, which triggers the clone and insertion.
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
Templates also offer a cleaner path for creating Shadow DOM. The common habit has been to assign a string to .innerHTML:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
That approach degrades into string concatenation as complexity grows, and it opens the door to XSS through user-supplied data. Working with DOM directly by appending template content to a shadow root avoids those problems.
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
Gotchas to watch for
A few implementation details can trip you up when using <template> in production:
- If you use modpagespeed, be aware of a known bug where templates that define inline
<style scoped>may have their CSS moved to the head during PageSpeed rewriting. - There's no way to "prerender" a template. You can't preload assets, run JavaScript, or fetch initial CSS ahead of time. The content stays dormant until it goes live.
- Nested templates don't cascade. Activating an outer template won't activate inner ones; you must manually activate each nested
<template>as well.
<template>
<ul>
<template>
<li>Stuff</li>
</template>
</ul>
</template>
How we used to do it
The path to a standard has been long, and developers previously improvised with two main strategies. Both work, but each has real drawbacks.
Offscreen DOM
Hiding template markup with the hidden attribute or display:none was a common early approach.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
This technique leverages the browser's native DOM handling and keeps the block from rendering. But it isn't inert: even hidden content triggers network requests, such as an image download. It also makes styling painful, since the embedding page must prefix all CSS with the template's ID to scope rules, leaving you vulnerable to naming collisions.
Script overloading
Another approach, popularized by John Resig's 2008 Micro Templating utility and later by handlebars.js, overloads the <script> tag and treats its content as a string.
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
The browser doesn't render the script block, and since the type attribute isn't "text/javascript", its contents aren't parsed as JavaScript. The trade-off is security: this pattern encourages .innerHTML and runtime string parsing, which can easily lead to XSS vulnerabilities when processing user-supplied data.
Why the standard matters
There's a satisfying pattern here: a library popularizes an approach, and the platform eventually standardizes it. Just as jQuery made DOM selection with CSS selectors commonplace before querySelector() became native, client-side templating libraries have paved the way for <template>.
The new element doesn't just standardize the practice — it removes the need for workarounds that have persisted since 2008. Having a native, inert, side-effect-free container for markup makes client-side templating more maintainable and, importantly, safer than the hacks that came before.



