Template Element
Learn how the HTML <template> element stores inert, reusable markup and how to clone its content with JavaScript to build dynamic UIs efficiently.
The HTML <template> element lets you declare a chunk of markup in your page that the browser parses but does not render. The content sits ready in the document, inert, until your JavaScript clones it and inserts it into the live DOM. This makes <template> the idiomatic way to define a "blueprint" for repeated UI — list rows, cards, modal dialogs — without building DOM nodes by hand or pasting HTML strings into innerHTML.
This chapter covers what makes template content special, how to clone and insert it correctly, how to wire up events on cloned nodes, and the gotchas that bite first-time users. Templates are also a building block of Web Components, where they pair naturally with custom elements and the Shadow DOM.
Understanding the Template Element
The <template> element acts as a container for storing HTML that isn't immediately rendered. What makes its content inert is the key idea:
- It is not displayed — the markup never paints, regardless of CSS.
- Its resources are not fetched —
<img>,<video>, and<script>inside a template do not load or run until the content is cloned into the live document. - Scripts inside it do not execute, and IDs inside it do not collide with the rest of the page until activated.
This is fundamentally different from hiding an element with display: none. A display: none element is still part of the live DOM: its images load, its scripts run, and document.getElementById finds elements inside it. Template content lives in a separate, document-fragment-backed tree, so none of that happens until you opt in by cloning it.
<body>
<div>You won't see the template, as it's not activated using JS.</div>
<div id="template-container">
<template id="my-template">
<h1>Hidden!</h1>
</template>
</div>
</body>Cloning Templates for Dynamic Content
A template is only a blueprint — it produces no visible output on its own. To use it, you read its content property (a DocumentFragment), clone that fragment, fill in your data, and append the clone to the page. Because you clone for each instance, one template can produce as many independent copies as you need, each with its own data.
There are two ways to clone:
template.content.cloneNode(true)— clones the fragment in place. Thetrueargument means a deep clone (include all descendants); without it you'd copy only the empty fragment.document.importNode(template.content, true)— does the same deep clone but explicitly imports the nodes into the current document. This matters when the template lives in a different document (for example, content pulled in via an<iframe>or<link rel="import">). For same-document templates the two are equivalent;importNodeis the safer default.
Always clone the template content before inserting it. If you append template.content directly, you move the original nodes out of the template into the DOM — the template is then empty and the next clone produces nothing. Cloning leaves the blueprint intact for reuse.
<head>
<style>
.card {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin: 10px;
width: 200px;
}
.card h3 {
margin: 0;
}
</style>
</head>
<body>
<div id="template-container">
<!-- Template element -->
<template id="card-template">
<div class="card">
<h3 id="card-title">Title</h3>
<p id="card-content">Content goes here...</p>
</div>
</template>
</div>
<div id="card-container">
<!-- Cards will be inserted here -->
</div>
<script>
// Data for multiple cards
const cardData = [
{ title: 'Card 1', content: 'This is the first card.' },
{ title: 'Card 2', content: 'This is the second card.' },
{ title: 'Card 3', content: 'This is the third card.' }
];
// Function to create and insert cards
function createCards(data) {
const template = document.getElementById('card-template');
data.forEach(item => {
const clone = document.importNode(template.content, true);
// Customize the cloned content
clone.querySelector('#card-title').textContent = item.title;
clone.querySelector('#card-content').textContent = item.content;
// Insert the cloned content into the DOM
document.getElementById('card-container').appendChild(clone);
});
}
// Create cards with the provided data
createCards(cardData);
</script>
</body>Enhancing Interactivity with JavaScript
The content property of a template returns a DocumentFragment that holds the inert markup. You can query and read it (template.content.querySelector(...)) without touching the live page, but a cleaner pattern is to clone first and then wire up events on the clone — that way each instance gets its own listeners. For deeper coverage of attaching handlers, see Event Handling in the DOM.
The example below defines two templates: a button and a content card. Clicking the button clones the card template and appends a fresh copy each time.
<body>
<div id="template-container">
<!-- Button Template -->
<template id="button-template">
<button id="show-content-btn">Add a content card</button>
</template>
<!-- Content Template -->
<template id="content-template">
<div class="content">
<h2>Dynamic Content</h2>
<p>This content is added dynamically when the button is clicked.</p>
</div>
</template>
</div>
<div id="button-container">
<!-- Button will be inserted here -->
</div>
<div id="content-container">
<!-- Content will be displayed here -->
</div>
<script>
// Function to display template content
function displayTemplateContent() {
// Get the content template
const contentTemplate = document.getElementById('content-template');
// Access the .content property and clone it
const contentClone = document.importNode(contentTemplate.content, true);
// Display the cloned content
document.getElementById('content-container').appendChild(contentClone);
}
// Insert the button template into the DOM
function insertButton() {
// Get the button template
const buttonTemplate = document.getElementById('button-template');
const buttonClone = document.importNode(buttonTemplate.content, true);
// Add event listener to the button
buttonClone.querySelector('#show-content-btn').addEventListener('click', displayTemplateContent);
// Insert the button into the DOM
document.getElementById('button-container').appendChild(buttonClone);
}
// Call the function to insert the button when the page loads
insertButton();
</script>
</body>In this example:
- We have two templates: one for a button (
button-template) and one for content (content-template). - The
insertButtonfunction clones the button template and inserts it into the DOM. It also attaches an event listener to the button to call thedisplayTemplateContentfunction when clicked. - The
displayTemplateContentfunction clones the content template and inserts it into the DOM each time the button is clicked. - The button is inserted into the DOM when the page loads by calling
insertButton.
Thus, when you click on the "Try it yourself" button, you'll see a button labeled "Add a content card." Each time you click it, a new card from the content template is added to the page, demonstrating dynamic insertion driven by user interaction.
Common Gotchas
A few pitfalls account for most template bugs:
- Forgetting to clone. Appending
template.contentdirectly empties the template. Always clone first. - Querying after appending.
appendChild(clone)empties the fragment into the DOM, so the clone's nodes become children of the target. Read or modify the clone (clone.querySelector(...)) before you append it — afterward the fragment is empty. - Duplicate IDs. IDs written inside a template are fine while inert, but once you clone it many times, each copy carries the same ID — and duplicate IDs are invalid HTML. Prefer classes,
data-*attributes, or scoped queries on the clone rather thandocument.getElementByIdfor per-instance lookups. - Expecting scripts to run. A
<script>inside a template does not execute on clone unless you re-create it. Keep behavior in your JavaScript, not in template markup.
Browser Support
The <template> element is part of the HTML Living Standard and is supported in every modern browser (Chrome, Firefox, Safari, Edge). No polyfill is needed for current browsers, which makes it a safe, dependency-free choice for client-side templating.
Conclusion
The <template> element gives you a native, framework-free way to define reusable markup and instantiate it on demand. Because its content is inert until cloned, it avoids the wasted rendering and resource loading of hidden elements, and it sidesteps the security and parsing quirks of building HTML from strings. Clone with cloneNode(true) or importNode, populate the clone, then append — and you have an efficient pattern that scales from a single card to a whole component system. To see templates inside a full component, continue with Custom Elements and Web Components.