⚡IntermediateHTML APIs and Web Components

Custom Elements

Create reusable HTML elements with the Custom Elements API and HTMLElement lifecycle callbacks.

Overview

Custom Elements let you define your own HTML elements with custom names and behavior. You extend HTMLElement, define lifecycle callbacks, and register the element with customElements.define(). After that, you can use <my-element> like any native HTML element. This is the foundation of Web Components a suite of standards that includes Custom Elements, Shadow DOM, and HTML Templates. Lifecycle callbacks: - connectedCallback(): called when element is added to DOM - disconnectedCallback(): called when removed - attributeChangedCallback(name, old, new): called when observed attributes change - adoptedCallback(): called when moved to a new document

Key Points

  • •Custom element names must contain a hyphen (e.g., my-button, user-card)
  • •Extend HTMLElement and call super() in constructor
  • •customElements.define("tag-name", ClassName) registers the element
  • •connectedCallback runs when the element mounts to the DOM
  • •observedAttributes + attributeChangedCallback reacts to attribute changes
  • •Custom elements are available globally once defined no import needed in HTML

Examples

A simple user-card custom element

<!-- HTML usage (works once the script defines it) -->
<user-card name="Alice" role="Developer"></user-card>
<user-card name="Bob" role="Designer"></user-card>

<script>
class UserCard extends HTMLElement {
  constructor() {
    super();
  }

  // Called when element is inserted into the DOM
  connectedCallback() {
    const name = this.getAttribute('name') || 'Unknown';
    const role = this.getAttribute('role') || 'Member';

    this.innerHTML = `
      <div class="user-card">
        <h3>${name}</h3>
        <p>${role}</p>
      </div>
    `;
  }
}

// Register the custom element
customElements.define('user-card', UserCard);
</script>

Reacting to attribute changes

class ProgressBar extends HTMLElement {
  static get observedAttributes() {
    return ['value', 'max'];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    this.render();
  }

  render() {
    const value = parseInt(this.getAttribute('value') || '0');
    const max   = parseInt(this.getAttribute('max') || '100');
    const pct   = Math.round((value / max) * 100);

    this.innerHTML = `
      <div class="progress-track">
        <div class="progress-fill" style="width: ${pct}%"></div>
        <span>${pct}%</span>
      </div>
    `;
  }
}

customElements.define('progress-bar', ProgressBar);

Usage: <progress-bar value="40" max="100"></progress-bar>

Extending a built-in element (customized built-in)

class FancyButton extends HTMLButtonElement {
  connectedCallback() {
    this.classList.add('btn-fancy');
    this.addEventListener('click', () => {
      this.animate([{ opacity: 1 }, { opacity: 0.5 }, { opacity: 1 }], 300);
    });
  }
}

customElements.define('fancy-button', FancyButton, { extends: 'button' });

Usage: <button is="fancy-button">Click Me</button>

Common Mistakes

  • Not including a hyphen in the custom element name single-word names are reserved for HTML
  • Forgetting super() in the constructor required when extending HTMLElement
  • Manipulating children/attributes in constructor do this in connectedCallback instead

Note

Custom elements are great for design system components and framework-independent widgets. For simple encapsulation without a framework, custom elements + shadow DOM (Web Components) provide a native solution.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What naming constraint applies to custom element tag names?