Custom Elements and Shadow DOM
Define your own HTML tags with encapsulated styles and behavior.
Web Components let you invent your own elements — real tags like <user-card> — that the browser treats as first-class citizens. Two pieces make this work.
Custom elements
You register a class with customElements.define('tag-name', ClassName). The one hard rule: the tag name must contain a hyphen, so it can never collide with a future built-in element.
Shadow DOM: true encapsulation
Calling attachShadow({ mode: 'open' }) gives your element a private DOM subtree. Styles you put inside stay inside; the page's CSS can't leak in and your styles can't leak out. This is the encapsulation that regular <div> components never had.
class UserCard extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = '<style>p{color:red}</style><p>Hi</p>';
}
}
customElements.define('user-card', UserCard);Lifecycle callbacks like connectedCallback (element added to the page) and attributeChangedCallback (a watched attribute changed) let your element react over time.
Example
When to use it
- A design system team defines a <ds-button> custom element that encapsulates styles and behavior shared across all company apps.
- A dashboard developer creates a <data-chart> web component so product teams drop charts into HTML without installing a library.
- A CMS plugin author registers <content-rating> as a custom element so editors can embed interactive star ratings in page content.
More examples
Defining and using a custom element
customElements.define() registers the class under a hyphenated tag name that is then usable in HTML.
<script>
class GreetBanner extends HTMLElement {
connectedCallback() {
const name = this.getAttribute("name") || "World";
this.innerHTML = `<p>Hello, <strong>${name}</strong>!</p>`;
}
}
customElements.define("greet-banner", GreetBanner);
</script>
<greet-banner name="SoundsCode"></greet-banner>Custom element with shadow DOM
Shadow DOM encapsulates the tooltip styles inside the component so they cannot bleed into the page.
<script>
class ToolTip extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>span { color: teal; cursor: help; }</style>
<span title="${this.getAttribute("tip")}">
<slot></slot>
</span>`;
}
}
customElements.define("tool-tip", ToolTip);
</script>
<p>HTML is a <tool-tip tip="HyperText Markup Language">markup</tool-tip> language.</p>Observed attributes and lifecycle callback
observedAttributes + attributeChangedCallback re-renders the component whenever its status attribute changes.
<script>
class StatusBadge extends HTMLElement {
static observedAttributes = ["status"];
attributeChangedCallback(name, oldVal, newVal) {
this.render();
}
connectedCallback() { this.render(); }
render() {
const s = this.getAttribute("status") || "unknown";
const color = s === "active" ? "green" : "red";
this.innerHTML = `<span style="color:${color}">${s}</span>`;
}
}
customElements.define("status-badge", StatusBadge);
</script>
<status-badge status="active"></status-badge>
Discussion