Web Components Basics
Building reusable custom elements with the Custom Elements API, Shadow DOM, and templates.
3 min read
Every framework you've heard of (React, Vue, Svelte) invents its own idea of a reusable, self-contained component. Web Components are the browser's native answer to the same problem — no build step or library required.
Defining a custom element
<script>
class GreetingCard extends HTMLElement {
connectedCallback() {
const name = this.getAttribute("name") || "there";
this.innerHTML = `<p>Hello, ${name}!</p>`;
}
}
customElements.define("greeting-card", GreetingCard);
</script>
<greeting-card name="Amara"></greeting-card>customElements.define() registers a new tag name (custom element names must contain a hyphen, like greeting-card — this is a hard requirement, so the browser can always tell your tags apart from any future built-in HTML element). connectedCallback() runs automatically the moment the element is inserted into the page, which is where you typically set up its initial content and behavior.
Shadow DOM: real encapsulation
<script>
class StatusBadge extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
span { padding: 4px 8px; border-radius: 999px; background: #dcfce7; color: #166534; }
</style>
<span><slot></slot></span>
`;
}
}
customElements.define("status-badge", StatusBadge);
</script>
<status-badge>Active</status-badge>attachShadow() creates a shadow root — a separate mini-DOM tree attached to the element. Styles defined inside a shadow root don't leak out to the rest of the page, and page-level styles don't leak in, solving the "my component's CSS collided with someone else's class name" problem without any naming convention (like BEM) needed. <slot> is a placeholder that gets filled with whatever content was written between the element's tags — here, the word "Active".
The <template> element
<template id="card-template">
<style>
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; }
</style>
<div class="card"><slot></slot></div>
</template>
<script>
class InfoCard extends HTMLElement {
connectedCallback() {
const template = document.getElementById("card-template");
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define("info-card", InfoCard);
</script>Content inside <template> is parsed by the browser but never rendered on its own — it exists purely as an inert blueprint to be cloned with JavaScript, as many times as needed. Combining <template> with Shadow DOM is the standard pattern: define the markup once, then stamp out an independent, style-isolated copy for every instance of the component.
Reacting to attribute changes
<script>
class StatusBadge extends HTMLElement {
static get observedAttributes() {
return ["variant"];
}
attributeChangedCallback(name, oldValue, newValue) {
this.className = `badge-${newValue}`;
}
}
customElements.define("status-badge", StatusBadge);
</script>
<status-badge variant="warning">Pending</status-badge>observedAttributes lists which attributes the element should watch, and attributeChangedCallback fires whenever one of them changes — the mechanism that lets a custom element stay in sync with attributes set (or later updated) in the markup, similar to how a framework component re-renders when a prop changes.
When to reach for this vs. a framework
Web Components shine for a component that needs to be dropped into many different contexts — a design system's building blocks, a widget embedded on third-party sites — where you can't assume any particular framework is present. For an entire application's UI, a framework's component model (with reactive state, routing, and a larger ecosystem) is usually still the more productive choice; the two aren't mutually exclusive; most frameworks can render Web Components alongside their own.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.