Mastering Custom Elements in HTML5

September, 19th 2024 3 min read

Custom Elements are part of the Web Components standard and allow you to create reusable, encapsulated HTML tags with your own behavior and styling. They help you build modular UI components without relying on heavy frameworks.

What Are Custom Elements?

Custom elements let you define new HTML tags backed by JavaScript classes. They support:

  • Encapsulated styles via Shadow DOM
  • Custom attributes
  • Lifecycle callbacks
  • Reusable component logic

Custom elements belong to two types:

  1. Autonomous elements (e.g., <user-card>)
  2. Customized built‑in elements extending built‑in tags (e.g., <button is="fancy-button">)

1. Creating a Basic Custom Element

Step 1: Define a Class

js
class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: "open" });
    shadow.innerHTML = `
      <style>
        p { color: blue; font-weight: bold; }
      </style>
      <p>Hello, I am a custom element!</p>
    `;
  }
}

Step 2: Register the Element

js
customElements.define("my-custom-element", MyCustomElement);

Step 3: Use It in HTML

html
<my-custom-element></my-custom-element>

2. Handling Attributes with attributeChangedCallback

js
class AlertBox extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `<p>Default alert</p>`;
  }

  static get observedAttributes() {
    return ["message"];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === "message") {
      this.shadowRoot.querySelector("p").textContent = newVal;
    }
  }
}

customElements.define("alert-box", AlertBox);

Usage:

html
<alert-box message="This is a custom alert!"></alert-box>

3. Shadow DOM Encapsulation Example

js
class FancyBox extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        div { padding: 16px; border: 2px solid green; }
      </style>
      <div><slot></slot></div>
    `;
  }
}

customElements.define("fancy-box", FancyBox);
html
<fancy-box>Inside Fancy Box</fancy-box>

4. Customized Built‑In Elements

js
class HighlightedButton extends HTMLButtonElement {
  constructor() {
    super();
    this.style.backgroundColor = "yellow";
    this.style.fontWeight = "bold";
  }
}

customElements.define("highlighted-button", HighlightedButton, { extends: "button" });

Usage:

html
<button is="highlighted-button">Click Me</button>

5. Advanced Example 1 — Toggle Switch

js
class ToggleSwitch extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    this.shadowRoot.innerHTML = `
      <style>
        .switch {
          width: 50px;
          height: 25px;
          border-radius: 20px;
          background: #ccc;
          position: relative;
          cursor: pointer;
        }
        .knob {
          width: 22px;
          height: 22px;
          background: white;
          border-radius: 50%;
          position: absolute;
          top: 1.5px;
          left: 1.5px;
          transition: transform .2s;
        }
        .on { background: #4caf50; }
        .on .knob { transform: translateX(25px); }
      </style>
      <div class="switch"><div class="knob"></div></div>
    `;

    this.switch = this.shadowRoot.querySelector(".switch");
    this.switch.addEventListener("click", () => this.toggle());
  }

  toggle() {
    this.switch.classList.toggle("on");
    this.dispatchEvent(new CustomEvent("change", { detail: this.isOn() }));
  }

  isOn() {
    return this.switch.classList.contains("on");
  }
}

customElements.define("toggle-switch", ToggleSwitch);

Usage:

html
<toggle-switch></toggle-switch>

6. Advanced Example 2 — User Card

js
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    const name = this.getAttribute("name") || "Unknown";
    const avatar = this.getAttribute("avatar") || "https://placehold.co/80";

    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          padding: 12px;
          border-radius: 8px;
          display: flex;
          gap: 12px;
          align-items: center;
        }
        img { border-radius: 50%; width: 60px; height: 60px; }
      </style>
      <div class="card">
        <img src="${avatar}" />
        <div>
          <h3>${name}</h3>
          <slot></slot>
        </div>
      </div>
    `;
  }
}

customElements.define("user-card", UserCard);

Usage:

html
<user-card name="Anton" avatar="/me.png">
  <p>Frontend Developer</p>
</user-card>

7. Advanced Example 3 — Modal Window

js
class CustomModal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    this.shadowRoot.innerHTML = `
      <style>
        .overlay {
          position: fixed;
          inset: 0;
          background: rgba(0,0,0,.5);
          display: none;
          justify-content: center;
          align-items: center;
        }
        .modal {
          background: white;
          padding: 20px;
          border-radius: 8px;
        }
        .open { display: flex; }
      </style>
      <div class="overlay">
        <div class="modal">
          <slot></slot>
          <button id="close">Close</button>
        </div>
      </div>
    `;

    this.overlay = this.shadowRoot.querySelector(".overlay");
    this.shadowRoot.querySelector("#close").onclick = () => this.hide();
  }

  show() { this.overlay.classList.add("open"); }
  hide() { this.overlay.classList.remove("open"); }
}

customElements.define("custom-modal", CustomModal);

Usage:

html
<custom-modal id="modal">Hello from modal!</custom-modal>

<script>
  document.getElementById("modal").show();
</script>

Conclusion

Custom elements let you extend HTML with your own reusable components. With Shadow DOM, lifecycle callbacks, and custom attributes, Web Components allow you to build modern, framework‑agnostic UI components that work anywhere.