Learn how to use the template element for reusable HTML fragments, and get an introduction to Shadow DOM and custom elements for building web components.
The <template> element holds HTML content that is not rendered when the page loads. It serves as a blueprint that you can clone and insert into the DOM with JavaScript.
<template id="card-template">
<div class="card">
<h3 class="card-title"></h3>
<p class="card-body"></p>
</div>
</template>Key characteristics:
<template> is inert — images do not load, scripts do not run, styles do not apply.template.content.template.content.cloneNode(true) to create a live copy.This is much cleaner than building DOM elements entirely in JavaScript or using innerHTML.
Here is the pattern for using a template:
// 1. Get the template
const template = document.getElementById('card-template');
// 2. Clone the content
const clone = template.content.cloneNode(true);
// 3. Fill in the data
clone.querySelector('.card-title').textContent = 'My Card';
clone.querySelector('.card-body').textContent = 'Card description here.';
// 4. Append to the DOM
document.getElementById('card-container').appendChild(clone);You can clone the template as many times as you need — each clone is independent. This is how you build dynamic lists, card grids, or any repeated UI pattern.
The <slot> element is used inside Shadow DOM to create insertion points where light DOM content appears.
Shadow DOM encapsulates a component's internal structure. Styles and markup inside a shadow tree do not leak out, and external styles do not leak in.
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card { border: 1px solid #333; padding: 1rem; }
</style>
<div class="card">
<slot name="title">Default Title</slot>
<slot>Default content</slot>
</div>
`;
}
}
customElements.define('my-card', MyCard);Usage:
<my-card>
<h3 slot="title">Custom Title</h3>
<p>This goes into the default slot.</p>
</my-card><slot name="...">) accept elements with a matching slot attribute.<slot>) catches all unassigned content.<slot> tags is fallback content shown when no content is provided.Traditionally, Shadow DOM requires JavaScript. Declarative Shadow DOM lets you define shadow trees directly in HTML using a <template> with shadowrootmode:
<my-card>
<template shadowrootmode="open">
<style>
.card { border: 1px solid #444; padding: 1rem; }
</style>
<div class="card">
<slot name="title">Default Title</slot>
<slot>Default content</slot>
</div>
</template>
<h3 slot="title">Server-Rendered Title</h3>
<p>This works without JavaScript!</p>
</my-card>This is especially useful for server-side rendering (SSR) — the shadow tree is attached immediately during HTML parsing, with no JavaScript needed for the initial render.
<!-- Template: not rendered, just a blueprint -->
<template id="item-template">
<li class="item">
<strong class="item-name"></strong>
<span class="item-desc"></span>
</li>
</template>
<!-- Container for cloned items -->
<ul id="item-list"></ul>
<script>
const template = document.getElementById('item-template');
const list = document.getElementById('item-list');
const items = [
{ name: 'HTML', desc: 'Structure' },
{ name: 'CSS', desc: 'Style' },
{ name: 'JS', desc: 'Behaviour' },
];
items.forEach(item => {
const clone = template.content.cloneNode(true);
clone.querySelector('.item-name').textContent = item.name;
clone.querySelector('.item-desc').textContent = ' — ' + item.desc;
list.appendChild(clone);
});
</script>What happens to content inside a <template> element when the page loads?