How to Use the Template Element in JavaScript for Reusable HTML
The <template> element is one of the simplest yet most underappreciated tools in the web platform. It allows you to declare fragments of HTML that the browser parses but does not render, keeping them completely inert until you explicitly activate them with JavaScript. No styles are applied, no scripts run, no images load, and no content appears on the page. It just sits there, ready to be cloned and inserted wherever you need it. In this guide, you will learn how the <template> element works, how to clone its content efficiently, and how to combine it with Shadow DOM to build performant, reusable web components.
The <template> Element: Inert HTMLâ
The <template> element is a mechanism for holding client-side content that is not rendered when the page loads. The browser parses the HTML inside <template> into a valid DOM structure, but it treats it as completely inactive.
What "Inert" Really Meansâ
When you place HTML inside a <template> tag, the browser does the following:
- Parses the HTML into DOM nodes (so you get a real DOM tree, not just a string).
- Does not render any of it on the page.
- Does not execute any
<script>tags inside it. - Does not load any resources (
<img>,<link>,<video>, etc.). - Does not apply any
<style>rules.
<template id="my-template">
<style>
.greeting { color: green; font-size: 24px; }
</style>
<div class="greeting">Hello from a template!</div>
<img src="photo.jpg" alt="This does NOT load until activated">
<script>
console.log('This does NOT execute until activated');
</script>
</template>
<p>The page loads normally. Nothing from the template appears.</p>
If you open this page, you will only see the paragraph text. The template content is invisible. No network request is made for photo.jpg. No console message appears. The green style does not affect anything on the page.
Verifying Inertnessâ
You can prove that the template content exists as parsed DOM but is not part of the visible document:
<template id="demo">
<p>I exist but I am not visible.</p>
</template>
<script>
const template = document.getElementById('demo');
// The template element itself is in the DOM
console.log(template);
// Output: <template id="demo">
// But querySelector on the document cannot find its children
console.log(document.querySelector('#demo p'));
// Output: null
// The content lives in a special DocumentFragment
console.log(template.content);
// Output: #document-fragment
// Inside that fragment, the parsed nodes exist
console.log(template.content.querySelector('p').textContent);
// Output: I exist but I am not visible.
</script>
The key takeaway here is that template children do not live in the main DOM tree. They live inside a special DocumentFragment accessible through the template.content property.
Where Can You Place <template>?â
The <template> element can appear almost anywhere in your HTML: inside <head>, <body>, <table>, <select>, or even inside other templates.
<head>
<template id="head-template">
<link rel="stylesheet" href="lazy-styles.css">
</template>
</head>
<body>
<template id="body-template">
<div class="card">Card content</div>
</template>
<table>
<template id="row-template">
<tr>
<td class="name"></td>
<td class="age"></td>
</tr>
</template>
</table>
</body>
Placing a <template> inside a <table> is particularly useful. Normally, browsers aggressively correct invalid table markup. If you try to put a <div> directly inside a <table>, the browser will move it outside. But <template> is a valid child of <table>, so you can safely store <tr> fragments inside it without the browser rearranging your DOM.
Template vs. Hidden Elementsâ
You might think: "Why not just use display: none or the hidden attribute?" There is a significant difference.
<!-- Approach 1: Hidden div - BAD for this purpose -->
<div id="hidden-content" hidden>
<img src="large-photo.jpg" alt="This WILL load!">
<script>console.log('This WILL execute!');</script>
</div>
<!-- Approach 2: Template - GOOD -->
<template id="template-content">
<img src="large-photo.jpg" alt="This will NOT load.">
<script>console.log('This will NOT execute.');</script>
</template>
| Feature | hidden / display: none | <template> |
|---|---|---|
| Parsed by browser | Yes | Yes |
| Rendered on page | No (hidden visually) | No |
| Resources loaded (images, etc.) | Yes | No |
| Scripts executed | Yes | No |
| Styles applied | Yes (just not visible) | No |
Found by document.querySelector() | Yes | No |
| Part of the DOM tree | Yes | No (lives in DocumentFragment) |
The <template> element is truly inert. Hidden elements are just invisible but still fully active.
Template vs. innerHTML Stringsâ
Another common approach is storing HTML as a string and using innerHTML to insert it:
// String approach
const htmlString = '<div class="card"><h2>Title</h2><p>Content</p></div>';
container.innerHTML = htmlString;
This works, but it has drawbacks:
- The browser must parse the string every time you use it.
- No syntax highlighting or editor support for the HTML inside the string.
- Prone to XSS if any part of the string contains user input.
- No IDE validation of the HTML structure.
With <template>, the HTML is parsed once by the browser, and you clone the resulting DOM nodes each time you need them. This is faster and safer for repeated use.
Cloning Template Content with cloneNode(true)â
The real power of <template> comes from cloning its content. You take the pre-parsed DOM fragment and stamp out copies wherever you need them.
Basic Cloningâ
<template id="alert-template">
<div class="alert">
<strong class="alert-title"></strong>
<p class="alert-message"></p>
</div>
</template>
<div id="notifications"></div>
<script>
const template = document.getElementById('alert-template');
const container = document.getElementById('notifications');
// Clone the template content
const clone = template.content.cloneNode(true);
// Populate the cloned content
clone.querySelector('.alert-title').textContent = 'Warning';
clone.querySelector('.alert-message').textContent = 'Disk space is running low.';
// Insert into the document - NOW it becomes visible and active
container.appendChild(clone);
</script>
Output on the page:
Warning
Disk space is running low.
Understanding cloneNode(true) vs. cloneNode(false)â
The cloneNode() method accepts a boolean argument:
cloneNode(true)performs a deep clone, copying the node and all of its descendants (children, grandchildren, etc.).cloneNode(false)performs a shallow clone, copying only the node itself without any children.
<template id="deep-vs-shallow">
<div class="parent">
<span class="child">Child text</span>
</div>
</template>
<script>
const template = document.getElementById('deep-vs-shallow');
// Deep clone - includes children
const deepClone = template.content.cloneNode(true);
console.log(deepClone.querySelector('.parent').innerHTML);
// Output: <span class="child">Child text</span>
// Shallow clone - empty DocumentFragment (no children copied)
const shallowClone = template.content.cloneNode(false);
console.log(shallowClone.childNodes.length);
// Output: 0
</script>
For templates, you almost always want cloneNode(true). Using cloneNode(false) on template.content gives you an empty DocumentFragment since the content node itself is the fragment, and shallow cloning does not copy its children.
Cloning Multiple Timesâ
One template can produce unlimited copies. Each clone is an independent DOM tree:
<template id="list-item-template">
<li class="item">
<span class="item-name"></span>
<span class="item-price"></span>
</li>
</template>
<ul id="product-list"></ul>
<script>
const template = document.getElementById('list-item-template');
const list = document.getElementById('product-list');
const products = [
{ name: 'Keyboard', price: '$75' },
{ name: 'Mouse', price: '$40' },
{ name: 'Monitor', price: '$350' },
{ name: 'Headphones', price: '$120' }
];
products.forEach(product => {
const clone = template.content.cloneNode(true);
clone.querySelector('.item-name').textContent = product.name;
clone.querySelector('.item-price').textContent = product.price;
list.appendChild(clone);
});
</script>
Output on the page:
âĸ Keyboard $75
âĸ Mouse $40
âĸ Monitor $350
âĸ Headphones $120
Each appendChild(clone) inserts an independent copy. Modifying one list item does not affect the others or the original template.
The DocumentFragment Advantageâ
When you call template.content.cloneNode(true), the result is a DocumentFragment. This matters for performance because appending a DocumentFragment to the DOM inserts all of its children in a single operation, triggering only one reflow instead of one per child node.
<template id="batch-template">
<div class="row">Row content</div>
</template>
<div id="container"></div>
<script>
const template = document.getElementById('batch-template');
const container = document.getElementById('container');
// Building up a fragment with many clones
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const clone = template.content.cloneNode(true);
clone.querySelector('.row').textContent = `Row ${i + 1}`;
fragment.appendChild(clone);
}
// Single DOM insertion - one reflow for 1000 elements
container.appendChild(fragment);
</script>
This is significantly faster than calling container.appendChild() 1000 times individually.
Important: The Clone Is Empty After Insertionâ
A DocumentFragment has a special behavior. When you append it to the DOM, its children are moved out of the fragment and into the target. The fragment itself becomes empty:
<template id="move-demo">
<p>Hello!</p>
</template>
<div id="target"></div>
<script>
const template = document.getElementById('move-demo');
const clone = template.content.cloneNode(true);
console.log(clone.childNodes.length);
// Output: 1 (the <p> element, possibly with whitespace text nodes)
document.getElementById('target').appendChild(clone);
console.log(clone.childNodes.length);
// Output: 0 (the fragment is now empty!)
</script>
This is normal DocumentFragment behavior. The original template.content remains untouched because you cloned it. But the clone itself is emptied after insertion.
A Common Mistake: Forgetting to Cloneâ
A frequent error is appending template.content directly without cloning:
// WRONG - This moves the content out of the template permanently
const template = document.getElementById('my-template');
container.appendChild(template.content);
// The template is now empty! You cannot reuse it.
console.log(template.content.childNodes.length);
// Output: 0
// CORRECT - Clone first, then append the clone
const template = document.getElementById('my-template');
const clone = template.content.cloneNode(true);
container.appendChild(clone);
// The template is preserved and reusable
console.log(template.content.childNodes.length);
// Output: still has all the original nodes
Always use template.content.cloneNode(true) instead of template.content directly. Appending the content without cloning destroys the template's reusability since DocumentFragment children are moved, not copied, on insertion.
Inserting Templates into Shadow DOMâ
The <template> element and Shadow DOM are natural partners. Templates provide the structure, and Shadow DOM provides the encapsulation. Together, they form the foundation of the Web Components architecture.
Basic Patternâ
The most common pattern is to define a template once, then clone it into a component's shadow root:
<template id="fancy-button-template">
<style>
:host {
display: inline-block;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
</style>
<button>
<slot>Default Text</slot>
</button>
</template>
<script>
class FancyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('fancy-button-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('fancy-button', FancyButton);
</script>
<!-- Usage -->
<fancy-button>Click Me</fancy-button>
<fancy-button>Submit</fancy-button>
<fancy-button>Cancel</fancy-button>
Each <fancy-button> gets its own shadow root with an independent clone of the template. The styles inside the template are scoped to each component's shadow tree.
Template Inside the Component Definitionâ
Instead of placing the template in the HTML document, you can define it programmatically within the component class. This is common when you want the component to be fully self-contained in a single JavaScript file:
const template = document.createElement('template');
template.innerHTML = `
<style>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
max-width: 300px;
font-family: system-ui, sans-serif;
}
.card-header {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.card-body {
color: #666;
line-height: 1.5;
}
.card-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #eee;
}
</style>
<div class="card">
<div class="card-header"><slot name="header">Card Title</slot></div>
<div class="card-body"><slot>Default content goes here.</slot></div>
<div class="card-footer"><slot name="footer"></slot></div>
</div>
`;
class InfoCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('info-card', InfoCard);
<info-card>
<span slot="header">Project Update</span>
<p>The new feature has been deployed successfully to production.</p>
<span slot="footer">Updated 2 hours ago</span>
</info-card>
Notice that the template variable is created once, outside the class. Every instance of <info-card> clones from the same pre-parsed template. This is more efficient than setting innerHTML inside the constructor for every instance.
Why Templates Are Faster Than innerHTML in Shadow DOMâ
Consider two approaches for populating a shadow root:
// Approach 1: innerHTML every time (slower for many instances)
class WidgetA extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>.box { color: red; }</style>
<div class="box">Content</div>
`;
// The browser parses this HTML string EVERY time a new
// <widget-a> is created
}
}
// Approach 2: Clone from template (faster for many instances)
const widgetBTemplate = document.createElement('template');
widgetBTemplate.innerHTML = `
<style>.box { color: blue; }</style>
<div class="box">Content</div>
`;
// The browser parses this HTML string ONCE
class WidgetB extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(widgetBTemplate.content.cloneNode(true));
// Cloning pre-parsed DOM nodes is faster than parsing HTML
}
}
With Approach 1, if you create 100 <widget-a> elements, the browser parses the same HTML string 100 times. With Approach 2, the HTML is parsed once, and the browser clones the resulting DOM nodes 100 times. Cloning is a much cheaper operation than parsing.
A Complete Interactive Exampleâ
Here is a more realistic component that combines templates, Shadow DOM, and event handling:
const todoTemplate = document.createElement('template');
todoTemplate.innerHTML = `
<style>
:host {
display: block;
margin: 8px 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 8px 12px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
transition: background-color 0.2s;
}
.todo-item.done {
background: #f0fff0;
}
.todo-item.done .todo-text {
text-decoration: line-through;
color: #999;
}
input[type="checkbox"] {
margin-right: 12px;
width: 18px;
height: 18px;
cursor: pointer;
}
.todo-text {
flex: 1;
font-size: 15px;
}
.delete-btn {
background: none;
border: none;
color: #e74c3c;
font-size: 18px;
cursor: pointer;
padding: 0 4px;
opacity: 0;
transition: opacity 0.2s;
}
.todo-item:hover .delete-btn {
opacity: 1;
}
</style>
<div class="todo-item">
<input type="checkbox" />
<span class="todo-text"></span>
<button class="delete-btn" aria-label="Delete">×</button>
</div>
`;
class TodoItem extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(todoTemplate.content.cloneNode(true));
this._checkbox = shadow.querySelector('input[type="checkbox"]');
this._text = shadow.querySelector('.todo-text');
this._item = shadow.querySelector('.todo-item');
this._deleteBtn = shadow.querySelector('.delete-btn');
}
connectedCallback() {
this._text.textContent = this.getAttribute('text') || 'Untitled task';
this._checkbox.addEventListener('change', () => {
this._item.classList.toggle('done', this._checkbox.checked);
this.dispatchEvent(new CustomEvent('todo-toggle', {
bubbles: true,
composed: true,
detail: { done: this._checkbox.checked }
}));
});
this._deleteBtn.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('todo-delete', {
bubbles: true,
composed: true
}));
this.remove();
});
}
get done() {
return this._checkbox.checked;
}
set done(value) {
this._checkbox.checked = value;
this._item.classList.toggle('done', value);
}
}
customElements.define('todo-item', TodoItem);
<div id="todo-list">
<todo-item text="Learn about template elements"></todo-item>
<todo-item text="Build a web component"></todo-item>
<todo-item text="Deploy to production"></todo-item>
</div>
<script>
document.getElementById('todo-list').addEventListener('todo-toggle', (e) => {
const item = e.target;
console.log(`"${item.getAttribute('text')}" is now ${e.detail.done ? 'done' : 'pending'}`);
});
document.getElementById('todo-list').addEventListener('todo-delete', (e) => {
console.log(`Deleted: "${e.target.getAttribute('text')}"`);
});
</script>
This example shows the full workflow:
- A
<template>is created once with all the markup and scoped styles. - Each
<todo-item>clones the template into its own shadow root. - The component is fully encapsulated, meaning page styles cannot interfere.
- Events bubble up through the shadow boundary with
composed: true.
Using Templates Defined in HTML (Multi-Component Page)â
For pages with multiple components, you can define all templates in the HTML and reference them by ID:
<template id="header-template">
<style>
header {
background: #2c3e50;
color: white;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 20px; font-weight: bold; }
nav a {
color: #ecf0f1;
text-decoration: none;
margin-left: 16px;
}
</style>
<header>
<div class="logo"><slot name="logo">My App</slot></div>
<nav><slot name="nav"></slot></nav>
</header>
</template>
<template id="footer-template">
<style>
footer {
background: #34495e;
color: #bdc3c7;
text-align: center;
padding: 12px;
font-size: 14px;
}
</style>
<footer>
<slot>© 2024 My Company</slot>
</footer>
</template>
<script>
class AppHeader extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const tmpl = document.getElementById('header-template');
shadow.appendChild(tmpl.content.cloneNode(true));
}
}
class AppFooter extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const tmpl = document.getElementById('footer-template');
shadow.appendChild(tmpl.content.cloneNode(true));
}
}
customElements.define('app-header', AppHeader);
customElements.define('app-footer', AppFooter);
</script>
<!-- Usage -->
<app-header>
<span slot="logo">My Website</span>
<span slot="nav">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</span>
</app-header>
<main>
<p>Page content here...</p>
</main>
<app-footer></app-footer>
This approach keeps the HTML templates visible and editable in the markup, which some teams find easier to work with than embedding them in JavaScript strings.
Templates Without Shadow DOMâ
While templates shine in combination with Shadow DOM, you can also use them without it for general-purpose DOM generation:
<template id="notification-template">
<div class="notification">
<span class="notification-icon"></span>
<span class="notification-message"></span>
<button class="notification-close">×</button>
</div>
</template>
<div id="notification-area"></div>
<script>
function showNotification(message, type = 'info') {
const template = document.getElementById('notification-template');
const clone = template.content.cloneNode(true);
const icons = { info: 'âšī¸', success: 'â
', warning: 'â ī¸', error: 'â' };
clone.querySelector('.notification-icon').textContent = icons[type];
clone.querySelector('.notification-message').textContent = message;
const notification = clone.querySelector('.notification');
notification.classList.add(`notification-${type}`);
clone.querySelector('.notification-close').addEventListener('click', () => {
notification.remove();
});
document.getElementById('notification-area').appendChild(clone);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (notification.parentNode) notification.remove();
}, 5000);
}
// Usage
showNotification('File saved successfully!', 'success');
showNotification('Check your network connection.', 'warning');
</script>
Without Shadow DOM, the styles are not scoped, so you would need to provide global CSS for .notification classes. But the template still provides the benefits of pre-parsed DOM and efficient cloning.
Summaryâ
| Concept | Key Point |
|---|---|
<template> element | Holds HTML that is parsed but not rendered, loaded, or executed |
template.content | A DocumentFragment containing the parsed nodes |
cloneNode(true) | Deep-clones the content for reuse; always use true |
| Inertness | No images load, no scripts run, no styles apply until inserted |
| Template vs. hidden | Templates are truly inactive; hidden elements still load resources |
| Template vs. innerHTML | Templates parse once and clone many; innerHTML parses every time |
| With Shadow DOM | Clone template content into shadow root for encapsulated components |
| Performance | Define template once outside class, clone per instance |
| DocumentFragment behavior | Fragment empties itself upon insertion; always clone, never move |
| Without Shadow DOM | Templates work for any repeated DOM generation, not just components |
The <template> element is the backbone of efficient web component creation. Combined with Shadow DOM for encapsulation and <slot> for content projection, it provides everything you need to build reusable, performant, and maintainable custom elements.