Creating Reusable HTML Tags with Custom Elements
Custom Elements are a foundational capability of the modern Web
Components standard, enabling web developers to invent their own fully
functional, reusable HTML tags powered by JavaScript. By extending the
browser’s built-in HTMLElement class, developers can
encapsulate custom user interface logic, define reactive attributes, and
control component lifecycles. This article explains how the Custom
Elements API works, how JavaScript powers these components, and how they
provide a native, framework-agnostic solution for modern web
development.
The Mechanism Behind Custom Elements
At the core of the Custom Elements API is the ability to map a standard JavaScript class to a custom HTML tag name. This is handled natively by the browser without requiring external libraries or build tools.
To register a custom element, you use the global
customElements.define() method:
class UserCard extends HTMLElement {
constructor() {
super();
this.innerHTML = `<p>Hello, I am a custom element!</p>`;
}
}
customElements.define('user-card', UserCard);Once defined, the tag can be used directly in HTML just like any standard element:
<user-card></user-card>Browsers require custom element names to contain a hyphen
(-). This naming convention prevents naming collisions with
existing and future standard HTML elements.
Powering Functionality with Lifecycle Callbacks
JavaScript drives the behavior of Custom Elements through specific lifecycle callbacks. These special methods execute automatically at different stages of an element’s existence in the DOM:
connectedCallback(): Invoked every time the custom element is appended to a document. This is ideal for running setup code, fetching data, or rendering templates.disconnectedCallback(): Invoked when the element is removed from the DOM. This is used for cleanup tasks such as removing event listeners or clearing timers.attributeChangedCallback(name, oldValue, newValue): Invoked whenever an observed attribute is added, removed, or modified.adoptedCallback(): Invoked when the element is moved to a new document (such as when working with<iframe>elements).
Making Elements Reactive with Observed Attributes
Custom elements can listen for changes to their HTML attributes to dynamically update their appearance or internal state using JavaScript.
To observe attributes, you define a static getter named
observedAttributes that returns an array of attribute
names:
class StatusBadge extends HTMLElement {
static get observedAttributes() {
return ['status'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'status') {
this.textContent = `Status: ${newValue}`;
}
}
}
customElements.define('status-badge', StatusBadge);Whenever the status attribute is modified via JavaScript
or standard HTML, the attributeChangedCallback fires
immediately to reflect the change.
Encapsulation with the Shadow DOM
While Custom Elements provide the structure and behavior, they are frequently paired with the Shadow DOM to achieve full encapsulation. The Shadow DOM creates an isolated DOM tree attached to the element, preventing component styles from leaking into the main document and shielding internal elements from global CSS selectors.
class SecureButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button {
background-color: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
</style>
<button><slot>Click Me</slot></button>
`;
}
}
customElements.define('secure-button', SecureButton);Advantages of Custom Elements
- Framework-Agnostic: Custom Elements work natively in any browser and can be consumed inside React, Vue, Angular, or vanilla JavaScript environments.
- Maintainability: HTML, CSS, and JavaScript logic are bundled into a single reusable unit.
- Standard-Based: Because Custom Elements are part of the official W3C web standards, they offer long-term stability and performance advantages over non-standard abstraction layers.