Skip to content

Custom Elements

Custom Elements represent one of the fundamental pillars of Web Components, enabling developers to define their own HTML tags and components. This capability extends the standard HTML vocabulary, allowing for the creation of reusable and encapsulated elements with custom behavior. Let's delve deeper into the world of Custom Elements and uncover how to harness their power.

Defining a Custom Element

To create a custom element, we use the class syntax in JavaScript to define a new class that extends the built-in HTMLElement class. This class encapsulates the element's behavior and properties. Once defined, we register it with the browser using customElements.define().

Example: Creating a Simple Custom Element


html
<my-custom-element></my-custom-element>
<script>
  class MyCustomElement extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `<p>Hello, World!</p>`;
    }
  }
  
  customElements.define('my-custom-element', MyCustomElement);
</script>

This example defines a simple custom element named my-custom-element that displays "Hello, World!" inside a shadow DOM. To use this element, simply add <code><my-custom-element></code> to your HTML.

note

Custom Elements v1 is supported in all modern browsers (Chrome 54+, Firefox 52+, Safari 10.1+, Edge 79+). Always verify browser compatibility if targeting legacy environments.

Lifecycle Callbacks

Custom elements have a set of lifecycle callbacks that allow developers to execute code at specific points in the element's lifecycle:

  • connectedCallback(): Invoked each time the custom element is appended to a document-connected element.
  • disconnectedCallback(): Invoked each time the custom element is disconnected from the document's DOM.
  • attributeChangedCallback(name, oldValue, newValue): Invoked each time one of the custom element's attributes is added, removed, or changed.
  • adoptedCallback(): Invoked each time the custom element is moved to a new document.
CallbackWhen it fires
connectedCallback()Element is added to the DOM
disconnectedCallback()Element is removed from the DOM
attributeChangedCallback(name, oldValue, newValue)An observed attribute changes
adoptedCallback()Element is moved to a new document

Example: Using Lifecycle Callbacks


javascript
<lifecycle-element></lifecycle-element>
<script>
class LifecycleElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        #status {
          color: blue;
          font-weight: bold;
        }
      </style>
      <p>Lifecycle Element</p>
      <p id="status">Element not connected</p>
    `;
  }

  connectedCallback() {
    this.shadowRoot.getElementById('status').textContent = 'Element connected to the page.';
  }

  disconnectedCallback() {
    this.shadowRoot.getElementById('status').textContent = 'Element disconnected from the page.';
  }
}

customElements.define('lifecycle-element', LifecycleElement);
</script>

Attributes and Properties

Custom elements can have attributes and properties to manage their state and behavior. Attributes are set directly in HTML and are always strings, while properties are set on the element's DOM object and can be any data type. Note that attributeChangedCallback only fires for attributes explicitly listed in the element's static get observedAttributes() method.

Example: Managing Attributes and Properties


javascript
<attribute-element id="element" data-content="Initial content"></attribute-element>
<button onclick="buttonClicked()">Click to change attribute</button>
<script>
  class AttributeElement extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `<p>Attribute Example: <span id="content"></span></p>`;
    }
  
    static get observedAttributes() {
      return ['data-content'];
    }
  
    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'data-content') {
        this.shadowRoot.getElementById('content').textContent = newValue;
      }
    }
  
    set content(value) {
      this.setAttribute('data-content', value);
    }
  
    get content() {
      return this.getAttribute('data-content');
    }
  }
  
  customElements.define('attribute-element', AttributeElement);

  function buttonClicked() {
    alert('button clicked!');
    const ourCustomElement = document.getElementById('element');
    ourCustomElement.content = 'New content';
  }
</script>

Here, the attribute-element updates its content based on the data-content attribute. The content property provides a convenient way to get and set this attribute programmatically.

Extending Built-In Elements

Custom elements can extend built-in HTML elements, adding new functionality while retaining their original behavior.

Example: Extending a Built-In Element


javascript
<button is="fancy-button">Click me!</button>
<script>
  class FancyButton extends HTMLButtonElement {
    constructor() {
      super();
      this.addEventListener('click', () => {
        alert('Fancy button clicked!');
      });
    }
  }
  
  customElements.define('fancy-button', FancyButton, { extends: 'button' });
</script>

Here, fancy-button extends the standard <button> element, adding an alert message when the button is clicked.

Custom Element Best Practices

  1. Use Shadow DOM: Always encapsulate your custom element's internal structure and styles using the Shadow DOM.
  2. Define Clear APIs: Provide clear and intuitive APIs for your custom elements through well-documented attributes and properties.
  3. Lifecycle Management: Properly manage the element's lifecycle callbacks to ensure robust behavior and avoid memory leaks.
  4. Accessibility: Ensure your custom elements are accessible by including appropriate ARIA roles and properties.
  5. Testing: Thoroughly test your custom elements across different browsers and environments to ensure compatibility and stability.

Conclusion

Custom elements offer a powerful way to extend HTML, enabling the creation of reusable, encapsulated components with custom behavior. By leveraging the features of custom elements, including lifecycle callbacks, attributes, properties, and the Shadow DOM, developers can build sophisticated and maintainable web applications.

Start experimenting with custom elements in your projects today, and unlock new possibilities for web development. The examples provided here are just the beginning—use them as a foundation to create your own innovative custom elements.

Practice

Which of the following statements about custom elements in JavaScript are true?

Dual-run preview — compare with live Symfony routes.