The template Element
Markup that is parsed but not rendered, waiting to be cloned. The clean way to build repeated content in JavaScript without writing HTML as strings.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
template holds markup that the browser parses and validates but does not render. Its contents are inert: images inside it are not fetched, scripts do not run, and nothing appears on the page until a script clones it and inserts the copy.
It is the answer to building repeated content in JavaScript. The alternative - assembling HTML as a string - is error prone, easy to get wrong in a way that introduces cross site scripting, and impossible for an editor to help with.
Syntax
<template id="row-template">
<tr>
<th scope="row" class="route"></th>
<td class="from"></td>
<td class="time"></td>
</tr>
</template>const template = document.getElementById("row-template");
const tbody = document.querySelector("tbody");
routes.forEach((route) => {
const row = template.content.cloneNode(true); // true = deep clone
row.querySelector(".route").textContent = route.number;
row.querySelector(".from").textContent = route.from;
row.querySelector(".time").textContent = route.firstBus;
tbody.append(row);
});Three points that matter:
template.contentis a document fragment holding the children. Thetemplateelement itself is not what you clone.cloneNode(true)takes a deep copy. Without the argument you get an empty fragment.- Appending a fragment inserts its children and leaves the fragment empty, which is why a fresh clone is needed on each iteration.
Why not a string
// fragile and unsafe
tbody.innerHTML += `<tr><td>${route.from}</td></tr>`;If route.from ever contains markup - because it came from a form, a database or an API - it is inserted as markup and executed. The template approach uses textContent, which inserts text as text, and the whole class of injection disappears.
innerHTML += has a second problem: it serialises and reparses the entire container on every call, destroying event listeners and losing form state.
Why not a hidden div
<div id="row-template" hidden>
<img src="/images/large.jpg" alt="">
</div>A hidden element is still in the DOM. That image is fetched, ids inside it are live and can collide, and any script inside it runs. A template avoids all of that: it is parsed but inert.
Example: a card list
<template id="course-card">
<article class="card">
<h3><a class="card-link" href="#"></a></h3>
<p class="card-summary"></p>
<dl>
<div><dt>Duration</dt><dd class="card-duration"></dd></div>
<div><dt>Intake</dt><dd class="card-intake"></dd></div>
</dl>
</article>
</template>
<div id="course-list" class="grid"></div>function renderCourses(courses) {
const template = document.getElementById("course-card");
const list = document.getElementById("course-list");
const fragment = document.createDocumentFragment();
for (const course of courses) {
const card = template.content.cloneNode(true);
const link = card.querySelector(".card-link");
link.textContent = course.title;
link.href = `/courses/${encodeURIComponent(course.slug)}`;
card.querySelector(".card-summary").textContent = course.summary;
card.querySelector(".card-duration").textContent = course.duration;
card.querySelector(".card-intake").textContent = course.intake;
fragment.append(card);
}
list.replaceChildren(fragment); // one insertion, one layout pass
}Building into a fragment first and inserting once means a single layout and paint rather than one per card.
Important rules
- Nothing inside a
templaterenders, loads or executes. - Access the contents through
.content. - Clone deeply, and clone fresh for each copy.
- Ids inside a template are not live until it is cloned, so they will collide once several copies are inserted. Prefer classes.
- A template may sit anywhere, including inside a
tableor aselect, where adivwould be invalid. - Contents are inert but still visible in the page source.
Common mistakes
- Cloning the
templateelement rather than itscontent. - Forgetting
trueincloneNodeand inserting nothing. - Reusing one clone in a loop, so only the last item appears.
- Putting ids in a template and creating duplicates once cloned.
- Using
innerHTMLwith untrusted values instead. - Using a hidden
divand paying for the images it loads. - Inserting inside the loop rather than into a fragment.
Best practices
- Use
templatefor anything repeated more than twice. - Fill it with
textContent, neverinnerHTML. - Use classes as hooks, not ids.
- Build into a fragment and insert once.
- Keep the template next to the container it fills.
- Encode any value interpolated into a URL.
Practice
- Render a list of ten items from an array using a template, inserting once.
- Put an image with a large file inside a hidden
divand inside atemplate, then compare the Network panel. - Clone without
trueand explain what appears. - Set a value containing markup with
textContentand then withinnerHTML, and describe the difference.