Custom Elements
Learn how to build Custom Elements in JavaScript: define a class, register it with customElements.define(), and use lifecycle callbacks.
Custom Elements are one of the core pillars of Web Components. They let you define your own HTML tags backed by a JavaScript class, extending the browser's built-in vocabulary with reusable, self-contained elements that carry their own structure, style, and behavior.
This page covers everything you need to ship a custom element: how to define and register one, the lifecycle callbacks the browser calls for you, how to react to attribute changes, how to extend built-in elements, and the practices that keep your components robust and accessible.
Two kinds of custom elements
The spec defines two flavors, and the distinction affects how you author and use them:
- Autonomous custom elements extend the generic
HTMLElementand are used as brand-new tags:<my-card></my-card>. This is the common case. - Customized built-in elements extend a specific built-in class (such as
HTMLButtonElement) and are used with theisattribute:<button is="fancy-button">. They inherit the host element's accessibility and behavior for free.
One rule applies to both: the tag name must contain a hyphen (my-card, not mycard). The hyphen is what tells the parser the tag is a custom element and prevents collisions with future standard tags.
Defining a Custom Element
To create an autonomous custom element, define a class that extends the built-in HTMLElement class, then register it with the browser using customElements.define(tagName, class). The class encapsulates the element's behavior; registration connects it to a tag name.
A common pattern is to build the element's internal DOM inside a shadow DOM so its markup and styles are isolated from the rest of the page.
Example: Creating a Simple Custom Element
<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 custom element named my-custom-element that displays "Hello, World!" inside a shadow DOM. To use it, just add <my-custom-element></my-custom-element> anywhere in your HTML. Note that custom elements have no self-closing form — always write the matching closing tag.
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.
| Callback | When 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 |
A useful mental model: the constructor runs once when the element instance is created (before it is in the DOM, so it must not touch attributes or children), while connectedCallback may run multiple times if the element is added, removed, and re-added. Do DOM-dependent setup in connectedCallback, and clean up listeners or timers in disconnectedCallback to avoid memory leaks.
Example: Using Lifecycle Callbacks
<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.
The key detail is attributeChangedCallback: it only fires for attributes explicitly listed in the element's static get observedAttributes() getter. If an attribute is not in that array, changing it triggers no callback. A common convention is to expose a getter/setter property that simply reflects to an attribute, so JavaScript code and HTML stay in sync.
Example: Managing Attributes and Properties
<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
Customized built-in elements extend a specific built-in class and are used with the is attribute. The big advantage is that they inherit the host element's semantics and accessibility — a <button is="fancy-button"> is still a real button to the keyboard and to screen readers.
Example: Extending a Built-In Element
<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. The third argument to customElements.define — { extends: 'button' } — tells the browser which tag this customized element applies to.
Safari does not support customized built-in elements (the is= form). For broad compatibility, prefer autonomous custom elements and re-implement the accessibility you need, or load a polyfill.
Custom Element Best Practices
- Use Shadow DOM: Always encapsulate your custom element's internal structure and styles using the Shadow DOM.
- Define Clear APIs: Provide clear and intuitive APIs for your custom elements through well-documented attributes and properties.
- Lifecycle Management: Properly manage the element's lifecycle callbacks to ensure robust behavior and avoid memory leaks.
- Accessibility: Ensure your custom elements are accessible by including appropriate ARIA roles and properties.
- 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.
Related topics
- Web Components — the umbrella standard custom elements belong to.
- Shadow DOM — encapsulate an element's internal DOM and styles.
- Shadow DOM Styling — style the inside of your component.
- Shadow DOM Slots and Composition — let users project content into your element.
- The
<template>Element — efficiently clone markup for your element's shadow root. - Class Basic Syntax — the class features custom elements are built on.