Templates & Web Components
As a site grows, you repeat the same pieces of HTML again and again: product cards, profile boxes, price labels. Web components are a set of browser standards that let you package such a piece once and reuse it as your own HTML tag, such as <profile-card>, on any page and with any framework.
In this lesson you will use the <template> element, create custom elements with a little JavaScript, keep their styles private with shadow DOM, and pass content into them with slots. You should be comfortable with the basics of the DOM in JavaScript before you start.
The template element
A <template> holds HTML that is not shown on the page. The browser reads it but does not display it, load its images or run its scripts. It is a stamp that you can copy with JavaScript whenever you need another instance.
The content is stored in the template's content property. Call content.cloneNode(true) to make a full copy, fill in the copy, and add it to the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Template</title>
<style>
.book { border: 1px solid #ccc; border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; max-width: 320px; }
</style>
</head>
<body>
<h1>New arrivals</h1>
<div id="shelf"></div>
<template id="book-template">
<article class="book">
<h2 class="title"></h2>
<p>By <span class="author"></span>, <strong class="price"></strong></p>
</article>
</template>
<script>
const books = [
{ title: "The Guide", author: "R. K. Narayan", price: 299 },
{ title: "Wings of Fire", author: "A. P. J. Abdul Kalam", price: 350 },
{ title: "Train to Pakistan", author: "Khushwant Singh", price: 275 },
];
const template = document.querySelector("#book-template");
const shelf = document.querySelector("#shelf");
for (const book of books) {
const copy = template.content.cloneNode(true);
copy.querySelector(".title").textContent = book.title;
copy.querySelector(".author").textContent = book.author;
copy.querySelector(".price").textContent = "₹" + book.price;
shelf.append(copy);
}
</script>
</body>
</html>The script fills each copy with textContent, not innerHTML. That way, even if a book title came from a user, it can never inject HTML or scripts into your page.
Custom elements
A custom element is a tag you invent and teach the browser about. You write a JavaScript class that extends HTMLElement and register it with customElements.define().
- The name must contain a hyphen and be all lowercase, such as
rupee-priceorprofile-card. The hyphen guarantees it can never clash with a future built-in HTML element. connectedCallback()runs when the element is added to the page. Do your setup there.- To react when an attribute changes, list it in
static observedAttributesand write anattributeChangedCallback()method.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Custom Element</title>
</head>
<body>
<p>Cricket bat: <rupee-price amount="2499"></rupee-price></p>
<p>Laptop: <rupee-price amount="64990"></rupee-price></p>
<p>Scooter: <rupee-price id="scooter" amount="129900"></rupee-price></p>
<button id="sale">Festival sale: 10% off the scooter</button>
<script>
const rupees = new Intl.NumberFormat("en-IN", {
style: "currency", currency: "INR", maximumFractionDigits: 0,
});
class RupeePrice extends HTMLElement {
static observedAttributes = ["amount"];
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
this.textContent = rupees.format(Number(this.getAttribute("amount")));
}
}
customElements.define("rupee-price", RupeePrice);
document.querySelector("#sale").addEventListener("click", () => {
const scooter = document.querySelector("#scooter");
scooter.setAttribute("amount", Number(scooter.getAttribute("amount")) * 0.9);
});
</script>
</body>
</html>Each <rupee-price> shows its amount in Indian format, such as ₹1,29,900. Clicking the button changes the amount attribute, and the element updates itself. Before define() runs, the browser treats an unknown tag like a plain inline element, so the page still works.
Shadow DOM: private markup and styles
When a component has its own internal HTML and CSS, you do not want page styles to break it, or its styles to leak onto the page. Shadow DOM solves this. Calling this.attachShadow({ mode: "open" }) gives the element a hidden, separate DOM tree called a shadow root. Styles inside it apply only inside it, and most page styles cannot reach in. Inherited properties such as color and font-family still flow in from the page, so components fit the surrounding text.
Inside the shadow root, the :host selector styles the custom element itself.
Slots: passing content in
A slot is a placeholder in the shadow DOM where the element's own children appear. A plain <slot> receives all children without a slot attribute. A named slot, <slot name="city">, receives the child with slot="city". Text between the slot tags is the fallback shown when nothing is provided.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Profile Card</title>
<style>
h2 { color: crimson; } /* page style: does not reach inside the card */
</style>
</head>
<body>
<h2>Our mentors</h2>
<profile-card>
<span>Priya Nair</span>
<span slot="city">Kochi</span>
</profile-card>
<profile-card>
<span>Farhan Qureshi</span>
</profile-card>
<script>
class ProfileCard extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) return; // already set up
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
:host { display: block; border: 1px solid #ccc; border-radius: 10px; padding: 12px; margin: 8px 0; max-width: 260px; }
h2 { margin: 0; font-size: 1.1rem; color: #0d47a1; }
</style>
<h2><slot>Unnamed mentor</slot></h2>
<p>City: <slot name="city">not given</slot></p>
`;
}
}
customElements.define("profile-card", ProfileCard);
</script>
</body>
</html>The page's h2 rule makes "Our mentors" red, but the headings inside the cards stay blue, because the shadow DOM keeps the two sets of styles apart. Farhan's card has no city, so the fallback text appears. Using innerHTML here is safe because the string is fixed in your own code; never build it from user input.
Declarative shadow DOM
You can also write a shadow root directly in HTML, with no JavaScript, by putting a <template shadowrootmode="open"> as the first child of an element. The browser turns it into a shadow root while reading the page. This lets a server send ready-made components that display before any script loads.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Declarative Shadow DOM</title>
</head>
<body>
<notice-box>
<template shadowrootmode="open">
<style>
:host { display: block; border-left: 4px solid orange; padding: 8px 12px; background: #fff8e1; }
</style>
<strong>Notice:</strong> <slot></slot>
</template>
The library will stay closed on Diwali.
</notice-box>
</body>
</html>When to use web components
Web components are useful for widgets you want to reuse across many pages or projects, such as a price label, a rating display or a date picker, especially when different teams use different frameworks. For a small site, plain HTML and CSS are often enough.
Remember that a custom element has no built-in meaning. A <buy-button> is not a button to a screen reader. Put real elements inside your components, such as a <button> in the shadow DOM, so they stay accessible, as covered in Accessibility Basics.
Common mistakes
- A name without a hyphen.
customElements.define("price", ...)throws an error; useprice-tag. - Defining the same name twice. The second
define()call throws an error, for example when a script is included twice. - Expecting page CSS to style shadow DOM content. Only inherited properties and CSS custom properties pass in. Style the inside in the component's own
<style>. - Building component HTML from user data with
innerHTML. UsetextContentfor anything that came from a user.
Key points
<template>holds inert HTML that you copy withtemplate.content.cloneNode(true).- Custom elements are classes that extend
HTMLElement, registered withcustomElements.define(), and their names must contain a hyphen. connectedCallback()runs when the element is added;observedAttributesandattributeChangedCallback()react to attribute changes.- Shadow DOM keeps a component's markup and styles private;
:hoststyles the element itself. - Slots place the element's children inside its shadow DOM, with named slots for specific parts.
<template shadowrootmode="open">creates a shadow root in plain HTML without JavaScript.
Exercise
Create a custom element called <star-rating> that reads a value attribute from 0 to 5 and shows that many filled stars (★) followed by empty stars (☆), for example ★★★☆☆ for 3. It should update when the value attribute changes. Show three ratings on a page for three Jaipur restaurants.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Star Rating</title>
</head>
<body>
<p>Laxmi Mishthan Bhandar: <star-rating value="5"></star-rating></p>
<p>Rawat Kachori: <star-rating value="4"></star-rating></p>
<p>Pink City Cafe: <star-rating value="3"></star-rating></p>
<script>
class StarRating extends HTMLElement {
static observedAttributes = ["value"];
connectedCallback() {
this.setAttribute("role", "img");
this.render();
}
attributeChangedCallback() { this.render(); }
render() {
const value = Math.min(5, Math.max(0, Number(this.getAttribute("value")) || 0));
this.textContent = "★".repeat(value) + "☆".repeat(5 - value);
this.setAttribute("aria-label", value + " out of 5 stars");
}
}
customElements.define("star-rating", StarRating);
</script>
</body>
</html>Math.min and Math.max keep the value between 0 and 5, so a wrong attribute cannot break repeat(). Changing value later calls attributeChangedCallback(), which redraws the stars. The role="img" and aria-label let screen readers say "4 out of 5 stars" instead of reading each star symbol.
Quick Quiz
1. Which is a valid custom element name?
Custom element names must be lowercase and contain a hyphen.
2. What happens to the content of a <template> when the page loads?
Template content is inert: not shown, images not loaded, scripts not run.
3. A page has h2 { color: red }. What colour is an <h2> inside a component's shadow DOM that has its own h2 { color: blue } rule?
Shadow DOM keeps page styles and component styles apart, so the component's own rule applies.
4. Where does <span slot="city">Kochi</span> appear inside a component?
Children with a slot attribute go into the slot with the matching name.
Learn HTML with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
