Custom Elements and Lifecycle Callbacks in JavaScript

Custom Elements are a foundational Web Components technology that allows developers to define, encapsulate, and register their own reusable HTML tags using modern JavaScript. This article explains the fundamentals of Custom Elements and details how to implement their standard lifecycle callbacks—connectedCallback, disconnectedCallback, adoptedCallback, and attributeChangedCallback—to manage component behavior effectively across different states of the Document Object Model (DOM).

What Are Custom Elements?

Custom Elements are custom HTML tags created by extending the base JavaScript HTMLElement class and registering them with the browser via the customElements.define() method. They enable developers to build modular, framework-agnostic user interface components with customized behavior and styling.

Custom Elements must contain a hyphen in their name (e.g., <user-card>) to avoid naming collisions with standard HTML tags.

Custom Element Lifecycle Callbacks

The browser executes specific lifecycle methods automatically at distinct stages of an element’s existence in the DOM. Implementing these callbacks allows developers to control rendering, initialize resources, and clean up memory.

1. constructor()

The constructor runs when an instance of the custom element is created (either via document.createElement() or when parsed from HTML).

2. connectedCallback()

Triggered every time the element is inserted into the DOM.

3. disconnectedCallback()

Triggered every time the element is removed from the DOM.

4. adoptedCallback()

Triggered when the element is moved to a new document (such as when moved between different <iframe> elements using document.adoptNode()).

5. attributeChangedCallback(name, oldValue, newValue)

Triggered when an observed attribute is added, removed, updated, or replaced on the element.

Implementation Example

Below is a complete implementation demonstrating how to create a custom element and define each lifecycle callback:

class CounterElement extends HTMLElement {
  // Define attributes to monitor
  static get observedAttributes() {
    return ['count'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  disconnectedCallback() {
    // Perform cleanup tasks here
  }

  adoptedCallback() {
    // Handle adoption into a new document
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  render() {
    const count = this.getAttribute('count') || 0;
    this.shadowRoot.innerHTML = `
      <style>
        div { font-family: sans-serif; padding: 8px; }
      </style>
      <div>Current Count: <strong>${count}</strong></div>
    `;
  }
}

// Register the custom element with the browser
customElements.define('counter-element', CounterElement);

Using this element in HTML is as straightforward as writing:

<counter-element count="5"></counter-element>

Whenever the count attribute is modified dynamically via JavaScript (e.g., element.setAttribute('count', '10')), attributeChangedCallback executes and re-renders the element’s shadow content.