Web Components in JavaScript

Introduction

Web Components are browser standards for reusable custom elements: Custom Elements, Shadow DOM, and HTML templates. Frameworks like React compile to similar ideas, but native components work without a build step in supporting browsers. This chapter introduces a minimal custom element you can extend later.

Prerequisites

Custom Element Basics

html
<hello-greeting name="Ada"></hello-greeting>
<script>
  class HelloGreeting extends HTMLElement {
    connectedCallback() {
      const name = this.getAttribute("name") ?? "Guest";
      this.textContent = `Hello, ${name}!`;
    }
  }
 
  customElements.define("hello-greeting", HelloGreeting);
</script>

Tag names must contain a hyphen (-) to avoid clashing with built-in tags.

Observed Attributes

javascript
class HelloGreeting extends HTMLElement {
  static get observedAttributes() {
    return ["name"];
  }
 
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "name") {
      this.textContent = `Hello, ${newValue ?? "Guest"}!`;
    }
  }
 
  connectedCallback() {
    this.attributeChangedCallback("name", null, this.getAttribute("name"));
  }
}
 
customElements.define("hello-greeting", HelloGreeting);

Shadow DOM — Encapsulated Styles

javascript
class InfoCard extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: "open" });
    shadow.innerHTML = `
      <style>
        :host { display: block; padding: 12px; border: 1px solid #ccc; }
        h2 { margin: 0; font-size: 1rem; }
      </style>
      <h2><slot name="title">Title</slot></h2>
      <p><slot>Body</slot></p>
    `;
  }
}
 
customElements.define("info-card", InfoCard);
html
<info-card>
  <span slot="title">Product</span>
  Notebook — 80 pages
</info-card>

Styles inside shadow root do not leak to the page; page CSS does not pierce shadow by default.

<template> Clone

html
<template id="row-tpl">
  <tr><td class="name"></td><td class="qty"></td></tr>
</template>
<tbody id="rows"></tbody>
<script>
  const tpl = document.getElementById("row-tpl");
  const tbody = document.getElementById("rows");
  const clone = tpl.content.cloneNode(true);
  clone.querySelector(".name").textContent = "Pen";
  clone.querySelector(".qty").textContent = "2";
  tbody.appendChild(clone);
</script>

When to Use Web Components

  • Design systems shared across multiple frameworks
  • Embed widgets on third-party sites
  • Progressive enhancement without a bundler

Large apps often still choose React/Vue/Svelte for ecosystem and tooling.

Mini Example: Click Counter Badge

javascript
class ClickBadge extends HTMLElement {
  #count = 0;
 
  connectedCallback() {
    this.#render();
    this.addEventListener("click", () => {
      this.#count += 1;
      this.#render();
    });
  }
 
  #render() {
    this.textContent = `Clicks: ${this.#count}`;
    this.style.cursor = "pointer";
  }
}
 
customElements.define("click-badge", ClickBadge);
html
<click-badge></click-badge>

FAQ

Browser support?

Custom elements and shadow DOM work in current evergreen browsers—verify targets for enterprise IE policies (legacy).

React and custom elements?

React passes props differently—use wrappers or libraries for seamless integration.

Lifecycle hooks?

connectedCallback, disconnectedCallback, adoptedCallback—clean up listeners in disconnectedCallback.

What comes next?

Canvas.