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

Try it yourself
Loading editor…
Press Run to see the result.

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.

Example · 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.

Example · html
<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.

Example · html
<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

  • Be the first to comment on this lesson.