Skip to content

Custom Elements

This lesson explains Custom Elements with clear examples, use cases, and best practices so you can build reusable Web Components confidently.

What Are Custom Elements?

Custom Elements are a core part of Web Components. They allow developers to create their own HTML elements with custom behavior, reusable logic, and lifecycle methods.

Instead of using only built-in elements like <button>, <input>, or <div>, you can define your own elements such as <app-button>, <user-card>, or <product-list>.

Concept Description
Custom Element A developer-defined HTML element
HTMLElement Base class used to create most custom elements
customElements.define() Registers a custom element with the browser
Lifecycle Callbacks Methods that run when the element is added, removed, or changed
Naming Rule Custom element names must contain a hyphen
Main Benefit Create reusable, framework-independent UI components

Basic Custom Element Example

class HelloMessage extends HTMLElement {
  constructor() {
    super();

    this.innerHTML = `
      <p>Hello from a Custom Element!</p>
    `;
  }
}

customElements.define("hello-message", HelloMessage);
<hello-message></hello-message>

The browser creates the custom element when it sees <hello-message> in the HTML.

Custom Element Class Structure

class AppCard extends HTMLElement {
  constructor() {
    super();

    this.innerHTML = `
      <article>
        <h2>App Card</h2>
        <p>This is a reusable custom element.</p>
      </article>
    `;
  }
}

customElements.define("app-card", AppCard);

A custom element is usually created by extending HTMLElement and registering the class with customElements.define().

How Custom Elements Work

Step What Happens
1. Create a class Extend HTMLElement
2. Call super() Initialize the parent element class
3. Add markup or logic Render HTML, attach events, or create Shadow DOM
4. Register element Use customElements.define("app-card", AppCard)
5. Use in HTML Place <app-card></app-card> anywhere in the page

Custom Element Naming Rules

Rule Valid Example Invalid Example
Must contain a hyphen user-card usercard
Use lowercase names app-button AppButton
Avoid built-in names site-header button
Use clear prefixes shop-product-card x1

Custom Element Lifecycle Callbacks

class UserBadge extends HTMLElement {
  constructor() {
    super();
    console.log("Element created");
  }

  connectedCallback() {
    console.log("Element added to the page");
  }

  disconnectedCallback() {
    console.log("Element removed from the page");
  }

  adoptedCallback() {
    console.log("Element moved to a new document");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(name, oldValue, newValue);
  }

  static get observedAttributes() {
    return ["name", "status"];
  }
}

customElements.define("user-badge", UserBadge);

Lifecycle callbacks help custom elements respond when they are created, added to the DOM, removed from the DOM, moved between documents, or updated through attributes.

Using connectedCallback()

class WelcomeMessage extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div class="alert alert-primary">
        Welcome to Web Components!
      </div>
    `;
  }
}

customElements.define("welcome-message", WelcomeMessage);
<welcome-message></welcome-message>

The connectedCallback() method runs when the element is added to the document. It is commonly used for rendering, setup, and event listeners.

Custom Element Attributes

class AlertBox extends HTMLElement {
  static get observedAttributes() {
    return ["type", "message"];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback() {
    this.render();
  }

  render() {
    const type = this.getAttribute("type") || "info";
    const message = this.getAttribute("message") || "Default alert message";

    this.innerHTML = `
      <div class="alert alert-${type}">
        ${message}
      </div>
    `;
  }
}

customElements.define("alert-box", AlertBox);
<alert-box
  type="success"
  message="Profile saved successfully">
</alert-box>

Attributes allow consumers to configure a custom element directly from HTML.

Custom Element Properties

class CounterButton extends HTMLElement {
  constructor() {
    super();
    this.count = 0;
  }

  connectedCallback() {
    this.render();
  }

  increment() {
    this.count += 1;
    this.render();
  }

  render() {
    this.innerHTML = `
      <button type="button">
        Count: ${this.count}
      </button>
    `;

    this.querySelector("button").addEventListener("click", () => {
      this.increment();
    });
  }
}

customElements.define("counter-button", CounterButton);
<counter-button></counter-button>

Properties are useful for storing component state and values that may not need to appear as HTML attributes.

Custom Elements With Shadow DOM

class ShadowCard extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" });

    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #dee2e6;
          border-radius: 1rem;
          padding: 1rem;
        }
      </style>

      <article class="card">
        <h2><slot name="title">Card Title</slot></h2>
        <div><slot></slot></div>
      </article>
    `;
  }
}

customElements.define("shadow-card", ShadowCard);
<shadow-card>
  <span slot="title">Profile</span>
  <p>This content is rendered through a slot.</p>
</shadow-card>

Shadow DOM gives the custom element private markup and styles, while slots allow external content to be inserted safely.

Customized Built-In Elements

class FancyButton extends HTMLButtonElement {
  connectedCallback() {
    this.classList.add("btn", "btn-primary");
  }
}

customElements.define("fancy-button", FancyButton, {
  extends: "button"
});
<button is="fancy-button">
  Save
</button>

Customized built-in elements extend native elements like HTMLButtonElement. However, autonomous custom elements such as <app-button> are more commonly used.

Custom Elements vs Built-In HTML Elements

Feature Built-In HTML Elements Custom Elements
Created By Browser Developer
Example <button>, <input> <app-button>, <user-card>
Behavior Predefined browser behavior Custom JavaScript behavior
Reusable UI Basic reusable primitives Application-specific reusable components
Lifecycle Native browser lifecycle Custom element lifecycle callbacks

When to Use Custom Elements

  • You want to create reusable UI components.
  • You need framework-independent components.
  • You want custom HTML tags for application features.
  • You need lifecycle methods for setup and cleanup.
  • You are building design system components.
  • You want components that can work with different frameworks.

Common Custom Element Use Cases

  • Buttons, badges, cards, modals, tabs, and accordions.
  • Design system components shared across applications.
  • Reusable widgets such as date pickers or search boxes.
  • Micro frontend components shared between teams.
  • Framework-independent UI libraries.
  • Application-specific elements like <user-profile>.

Custom Elements Best Practices

  • Always include a hyphen in custom element names.
  • Use clear and meaningful names such as user-card.
  • Call super() before using this in the constructor.
  • Use connectedCallback() for DOM setup and rendering.
  • Use disconnectedCallback() for cleanup.
  • Keep rendering logic in a reusable render() method.
  • Use Shadow DOM when you need style and markup encapsulation.
  • Document supported attributes, properties, events, slots, and parts.

Common Custom Element Mistakes

  • Using a custom element name without a hyphen.
  • Forgetting to call super() in the constructor.
  • Doing heavy DOM work inside the constructor.
  • Adding event listeners repeatedly without cleanup.
  • Forgetting to observe attributes with observedAttributes.
  • Re-rendering too often without checking changed values.
  • Not documenting the component public API.

Key Takeaways

  • Custom Elements let you create your own HTML tags.
  • Most custom elements extend HTMLElement.
  • Register elements with customElements.define().
  • Custom element names must include a hyphen.
  • Lifecycle callbacks help manage setup, updates, and cleanup.
  • Custom Elements are reusable and framework-independent.

Pro Tip

Build Custom Elements like a public API. Clearly document attributes, properties, events, slots, CSS Custom Properties, and CSS Parts so other developers can use your component safely.