This episode breaks down templates and the shadow DOM: the template element for storing unrendered markup, slots for content distribution, and style and structure encapsulation through custom elements.

Complex pages demand markup fragments that get reused over and over. Episode 18 covers HTML templates and basic shadow DOM: the template element for storing unrendered markup, slot for content distribution, and the shadow DOM that isolates styles and structure from the main document.
These concepts are the foundation of Web Components — a way to create your own HTML elements that behave and style themselves independently. Understanding them lets you build truly reusable interfaces without depending on a framework.
template stores an HTML fragment that isn't displayed when the page loads. Its contents only become live when cloned and inserted via JavaScript:
<template id="kartu-siswa">
<div class="kartu">
<h3></h3>
<p></p>
</div>
</template>Content inside template is not rendered, loads no images, and can't be seen by users until it's used. It's a clean way to keep markup together with its document.
To use a template, access its content property, clone it with cloneNode(true), then fill and insert:
const template = document.getElementById("kartu-siswa");
const fragmen = template.content.cloneNode(true);
fragmen.querySelector("h3").textContent = "Ayu Lestari";
fragmen.querySelector("p").textContent = "Kelas 3A";
document.body.appendChild(fragmen);The true argument on cloneNode ensures the entire DOM tree is copied, not just the outermost element. Each use produces a fresh copy, so the original markup stays intact.
The shadow DOM is a separate DOM tree attached to a host element. Create it by calling attachShadow:
const host = document.getElementById("widget-halo");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = "<p>Halo dari dalam shadow DOM</p>";The open mode makes the shadow root accessible from external JavaScript — try host.shadowRoot in the console to inspect its contents. The closed mode hides it from external access, but adds no real security; open is more common and easier to inspect.
Parent page CSS rules don't penetrate the shadow root by default. Conversely, styles inside the shadow don't leak out — that's the essence of encapsulation:
shadow.innerHTML = `
<style>
p { color: #2563eb; font-weight: 700; }
</style>
<p>Halo dari dalam shadow DOM</p>
`;The p inside the shadow isn't affected by the page's p { color: red } rule, and vice versa. This prevents style collisions in large projects.
The shadow DOM doesn't have to be completely closed. The slot element opens holes that accept content from outside, directed by the slot attribute:
<my-kartu>
<span slot="judul">Pengumuman</span>
<span slot="tanggal">10 Agustus 2026</span>
</my-kartu>Inside the shadow, slot name="judul" maps the span with the judul slot to its place. The original content stays in the light DOM and is styled by the parent page.
Combine everything: a custom element that attaches a shadow root and renders its slots when created:
class KartuKabar extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
.kartu { border: 1px solid #d1d5db; padding: 1rem; }
.judul { font-weight: 700; }
</style>
<div class="kartu">
<div class="judul"><slot name="judul"></slot></div>
<div><slot name="tanggal"></slot></div>
</div>
`;
}
}
customElements.define("my-kartu", KartuKabar);A custom element name must contain a hyphen, for example my-kartu, so it doesn't collide with built-in elements. After customElements.define, the <my-kartu> tag can be used anywhere on the page.
Create several cards from an array of data using templates and the shadow DOM:
const data = [
{ nama: "Ayu", kelas: "3A" },
{ nama: "Bima", kelas: "3B" },
{ nama: "Citra", kelas: "3C" },
];
for (const item of data) {
const fragmen = template.content.cloneNode(true);
fragmen.querySelector("h3").textContent = item.nama;
fragmen.querySelector("p").textContent = "Kelas " + item.kelas;
document.body.appendChild(fragmen);
}Replace the contents of data with your real list. Because the template is cloned on each iteration, no cards share the same elements — safe for dynamic data.
Adding a template itself to the document displays nothing — it stays inert. Always clone its content with cloneNode(true) before inserting.
document.querySelector doesn't reach into the shadow root. To search it, access it via host.shadowRoot.querySelector(...) when the mode is open.
Episode 18 introduces how to build isolated components: template for stored markup, slot for external content, and the shadow DOM for style and structure encapsulation through custom elements.
Key takeaways:
template stores markup that isn't rendered until cloned.cloneNode(true) creates a complete copy of the DOM tree.attachShadow creates a separate DOM tree on a host element.slot receives content from the light DOM via the slot attribute.In the next episode, episode 19, we'll cover practices for writing clean, maintainable HTML — indentation, heading hierarchy, class naming, and automated linting so your markup is comfortable for your team to read.