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.