T35: Web Components I - Custom Elements & Shadow DOM

What if you could invent your own HTML tag? <user-card>, <rating-stars>, <search-box>. Web Components let you do that with no framework and no build step. Two ingredients: Custom Elements define the tag, Shadow DOM seals its insides so nothing leaks in or out.

Custom Elements

A custom element is a class that extends HTMLElement, registered with the browser under a tag name. The tag name must contain a hyphen so the browser can tell it apart from built-ins. Registration is permanent for the page.

class GreetingBox extends HTMLElement {
    constructor() {
        super();
        this.textContent = "Hello from a custom element!";
    }
}

customElements.define("greeting-box", GreetingBox);

Now this tag works anywhere in your HTML:

<!-- In any page -->
<greeting-box></greeting-box>

Reading Attributes

A good custom element reads its own attributes to configure itself. Built-in elements do this: <img src="...">, <a href="...">. Your elements should too.

class GreetingBox extends HTMLElement {
    connectedCallback() {
        const name = this.getAttribute("name") || "friend";
        this.textContent = `Hello, ${name}!`;
    }
}
customElements.define("greeting-box", GreetingBox);

// <greeting-box name="Alice"></greeting-box>

Shadow DOM: The Sealed Interior

Shadow DOM attaches a private DOM tree to your element. Outside styles cannot reach in, inside styles cannot leak out. Without it, a stray h2 { color: red } anywhere on the page would repaint your widget.

class UserCard extends HTMLElement {
    connectedCallback() {
        const root = this.attachShadow({ mode: "open" });
        const name = this.getAttribute("name") || "Anonymous";
        root.innerHTML = `
            <style>
                :host { display: inline-block; padding: 1rem;
                       border: 1px solid #ddd; border-radius: 8px; }
                h2 { margin: 0; font-size: 1rem; color: #333; }
                p  { margin: 0.25rem 0 0; color: #666; }
            </style>
            <h2>${name}</h2>
            <p>Welcome back.</p>
        `;
    }
}
customElements.define("user-card", UserCard);
flowchart TD Page["Light DOM: the page"] Tag["user-card element (name=Alice)"] Shadow["Shadow DOM root"] H["h2: Alice"] P["p: Welcome back."] Style["scoped style block"] Page --> Tag Tag -->|attachShadow| Shadow Shadow --> Style Shadow --> H Shadow --> P

:host and ::part

Inside the shadow tree, the :host selector styles the custom element itself from the outside looking in. If you want to allow specific parts to be styled from outside, expose them with part="..." and consumers style via ::part().

<style>
    :host { display: block; }
    :host([featured]) { border-color: gold; }
    button { cursor: pointer; }
</style>
<button part="action">Click me</button>

/* In the outer page CSS */
user-card::part(action) { background: tomato; color: white; }

Key Takeaways

  • Extend HTMLElement, register with customElements.define("my-tag", Class) - tag must have a hyphen
  • Read attributes with getAttribute to configure the element from HTML
  • Shadow DOM seals your internal markup and styles from the rest of the page
  • Use :host to style the element itself, ::part to expose styling hooks to outside CSS
  • Web Components work in any framework or none - they are platform-native