HTML Template and Slot Elements
1. Introduction
The <template> element holds client-side content that is not rendered on page load but can be cloned and inserted into the document via JavaScript. Paired with <slot> elements inside Web Components’ Shadow DOM, it forms the foundation of the native HTML component model. Understanding templates is essential for working with Web Components and for any pattern that requires efficient, repeated DOM creation without string concatenation.
2. Concept
Template vs Other Dynamic Content Methods
| Method | Parsed on load? | Rendered on load? | Best For |
|---|---|---|---|
<template> |
Yes (HTML parsed) | No (inert) | Reusable DOM fragments; Web Components |
| innerHTML string | No (raw string) | On assignment | Simple dynamic content (XSS risk) |
| createElement API | N/A | On append | Programmatic, type-safe DOM building |
| Framework templates (JSX, Vue) | Compiled | Framework-managed | Component-driven apps |
<template> is completely inert โ images don’t load, scripts don’t execute, and styles don’t apply โ until the content is cloned and inserted into the document. This makes templates very efficient for defining reusable structures.template.content.cloneNode(true) to get a deep clone of the template’s DocumentFragment. Pass true to clone all descendants. Then populate data before appending to the DOM to avoid causing multiple reflows.innerHTML, template cloning is not vulnerable to XSS injection from the template definition itself. However, when you populate the clone with user-supplied data, always use textContent (not innerHTML) to avoid creating XSS vulnerabilities.3. Basic Example
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Template Demo</title></head>
<body>
<!-- Template: inert until cloned -->
<template id="product-card-tpl">
<article class="card">
<img class="card__img" src="" alt="" width="300" height="200" loading="lazy">
<div class="card__body">
<h3 class="card__title"></h3>
<p class="card__price"></p>
<button class="card__btn" type="button">Add to Cart</button>
</div>
</article>
</template>
<!-- Container where cards are inserted -->
<div id="product-grid"></div>
<script>
var products = [
{ id: 'PB15', name: 'ProBook 15', price: 'ยฃ1,299', img: '/probook.jpg', alt: 'ProBook 15 laptop' },
{ id: 'M4K', name: '4K Monitor', price: 'ยฃ249', img: '/monitor.jpg', alt: '32-inch 4K monitor' },
{ id: 'KBD', name: 'Mech Keyboard', price: 'ยฃ89', img: '/keyboard.jpg', alt: 'Mechanical keyboard' },
];
var template = document.getElementById('product-card-tpl');
var grid = document.getElementById('product-grid');
products.forEach(function(p) {
// Clone the template content
var clone = template.content.cloneNode(true);
// Populate โ using textContent, NOT innerHTML
clone.querySelector('.card__img').src = p.img;
clone.querySelector('.card__img').alt = p.alt;
clone.querySelector('.card__title').textContent = p.name;
clone.querySelector('.card__price').textContent = p.price;
clone.querySelector('.card__btn').dataset.productId = p.id;
grid.appendChild(clone);
});
</script>
</body>
</html>
4. How It Works
Step 1 โ The template Element is Inert
The browser parses the HTML inside <template> into a DocumentFragment accessible via template.content, but nothing is rendered. Images don’t load, scripts don’t run. This is significantly more efficient than hiding content with CSS.
Step 2 โ cloneNode(true)
template.content.cloneNode(true) creates a deep copy of the DocumentFragment including all descendants. Each clone is independent โ modifying one does not affect others. You can create as many instances as needed from one template definition.
Step 3 โ Populate Before Appending
Populate the clone’s elements with data before calling appendChild. This batches all DOM mutations before the browser reflows, improving performance. Setting src on an <img> only triggers a network request after the element is in the document.
Step 4 โ slot in Shadow DOM
When used with Web Components, <slot name="title"> inside a shadow template acts as a placeholder. Light DOM children with slot="title" on the host element are projected into the matching slot, combining the component’s template with consumer-provided content.
5. Real-World Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Comment Thread</title>
<style>
.comment { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
.comment__avatar { width: 40px; height: 40px; border-radius: 50%; vertical-align: middle; margin-right: 0.5rem; }
.comment__meta { font-size: 0.8rem; color: #6b7280; }
</style>
</head>
<body>
<h1>Comments</h1>
<template id="comment-tpl">
<article class="comment">
<header>
<img class="comment__avatar" src="" alt="" width="40" height="40">
<strong class="comment__author"></strong>
<time class="comment__meta"></time>
</header>
<p class="comment__body"></p>
<button class="comment__reply" type="button">Reply</button>
</article>
</template>
<section id="comment-list" aria-label="Comments"></section>
<script>
var comments = [
{ author: 'Alice', avatar: 'https://via.placeholder.com/40', date: '2025-03-10', dateDisplay: 'Mar 10', body: 'Great tutorial! Very clear explanations throughout.' },
{ author: 'Bob', avatar: 'https://via.placeholder.com/40', date: '2025-03-11', dateDisplay: 'Mar 11', body: 'The code examples are exactly what I needed. Bookmarked.' },
{ author: 'Carol', avatar: 'https://via.placeholder.com/40', date: '2025-03-12', dateDisplay: 'Mar 12', body: 'Could you cover CSS Grid in the next lesson?' },
];
var tpl = document.getElementById('comment-tpl');
var list = document.getElementById('comment-list');
comments.forEach(function(c) {
var clone = tpl.content.cloneNode(true);
clone.querySelector('.comment__avatar').src = c.avatar;
clone.querySelector('.comment__avatar').alt = c.author + ' avatar';
clone.querySelector('.comment__author').textContent = c.author;
var time = clone.querySelector('.comment__meta');
time.setAttribute('datetime', c.date);
time.textContent = c.dateDisplay;
clone.querySelector('.comment__body').textContent = c.body;
clone.querySelector('.comment__reply').dataset.author = c.author;
list.appendChild(clone);
});
</script>
</body>
</html>
6. Common Mistakes
❌ Using innerHTML to insert user data (XSS vulnerability)
<script>
clone.querySelector('.title').innerHTML = userInput; // XSS risk
</script>
✓ Always use textContent for user-supplied data
<script>
clone.querySelector('.title').textContent = userInput; // safe
</script>
❌ Not using cloneNode โ all variables point to the same fragment
<script>
var frag = template.content;
list.appendChild(frag); // fragment is moved, template is now empty
list.appendChild(frag); // appends nothing โ frag is already moved
</script>
✓ Always clone the template content before appending
<script>
list.appendChild(template.content.cloneNode(true));
list.appendChild(template.content.cloneNode(true));
</script>
7. Try It Yourself
8. Quick Reference
| API / Element | Purpose | Notes |
|---|---|---|
<template id> |
Defines inert reusable DOM fragment | Not rendered; not downloaded (images) |
template.content |
DocumentFragment containing template DOM | Read-only; clone before use |
content.cloneNode(true) |
Deep clone of template | Independent copy; safe to modify |
el.textContent = val |
Set text safely | No XSS risk; use for all user data |
parent.appendChild(clone) |
Insert clone into DOM | Triggers rendering and image load |
<slot name> |
Placeholder in shadow DOM template | Filled by light DOM slot=”name” children |