Using custom elements - Web APIs | MDN
Skip to search
Using custom elements
One of the key features of web components is the ability to create
custom elements
: that is, HTML elements whose behavior is defined by the web developer, that extend the set of elements available in the browser.
This article introduces custom elements, and walks through some examples.
Types of custom element
There are two types of custom element:
Autonomous custom elements
inherit from the HTML element base class
HTMLElement
. You have to implement their behavior from scratch.
Customized built-in elements
inherit from standard HTML elements such as
HTMLImageElement
or
HTMLParagraphElement
. Their implementation extends the behavior of select instances of the standard element.
Note:
Safari does not plan to support customized built-in elements. See the
is
attribute
for more information.
For both kinds of custom element, the basic steps to create and use them are the same:
You first
implement its behavior
by defining a JavaScript class.
You then
register the custom element
to the current page. You can also create
scoped registries
to limit definitions to a particular DOM subtree.
Finally, you can
use the custom element
in your HTML or JavaScript code.
Implementing a custom element
A custom element is implemented as a
class
which extends
HTMLElement
(in the case of autonomous elements) or the interface you want to customize (in the case of customized built-in elements). This class will not be called by you, but will be called by the browser. Immediately after defining the class, you should
the custom element, so you can create instances of it using standard DOM practices, such as writing the element in HTML markup, calling
document.createElement()
, etc.
Here's the implementation of a minimal custom element that customizes the
element:
js
class WordCount extends HTMLParagraphElement {
constructor() {
super();
// Element functionality written in here
Here's the implementation of a minimal autonomous custom element:
js
class PopupInfo extends HTMLElement {
constructor() {
super();
// Element functionality written in here
In the class
constructor
, you can set up initial state and default values, register event listeners and perhaps create a shadow root. At this point, you should not inspect the element's attributes or children, or add new attributes or children. See
Requirements for custom element constructors and reactions
for the complete set of requirements.
Custom element lifecycle callbacks
Once your custom element is registered, the browser will call certain methods of your class when code in the page interacts with your custom element in certain ways. By providing an implementation of these methods, which the specification calls
lifecycle callbacks
, you can run code in response to these events.
Custom element lifecycle callbacks include:
connectedCallback()
: Called each time the element is added to the document. The specification recommends that, as far as possible, developers should implement custom element setup in this callback rather than the constructor.
disconnectedCallback()
: Called each time the element is removed from the document.
connectedMoveCallback()
: When defined, this is called
instead of
connectedCallback()
and
disconnectedCallback()
each time the element is moved to a different place in the DOM via
Element.moveBefore()
. Use this to avoid running initialization/cleanup code in the
connectedCallback()
and
disconnectedCallback()
callbacks when the element is not actually being added to or removed from the DOM. See
Lifecycle callbacks and state-preserving moves
for more details.
adoptedCallback()
: Called each time the element is moved to a new document.
attributeChangedCallback()
: Called when attributes are changed, added, removed, or replaced. See
Responding to attribute changes
for more details about this callback.
Here's a minimal custom element that logs these lifecycle events:
js
// Create a class for the element
class MyCustomElement extends HTMLElement {
static observedAttributes = ["color", "size"];
constructor() {
// Always call super first in constructor
super();
connectedCallback() {
console.log("Custom element added to page.");
disconnectedCallback() {
console.log("Custom element removed from page.");
connectedMoveCallback() {
console.log("Custom element moved with moveBefore()");
adoptedCallback() {
console.log("Custom element moved to new page.");
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} has changed.`);
customElements.define("my-custom-element", MyCustomElement);
Lifecycle callbacks and state-preserving moves
The position of a custom element in the DOM can be manipulated just like any regular HTML element, but there are lifecycle side-effects to consider.
Each time a custom element is moved (via methods such as
Element.moveBefore()
or
Node.insertBefore()
), the
disconnectedCallback()
and
connectedCallback()
lifecycle callbacks are fired, because the element is disconnected from and reconnected to the DOM.
This might be your intended behavior. However, since these callbacks are typically used to implement any required initialization or cleanup code to run at the start or end of the element's lifecycle, running them when the element is moved (rather than removed or inserted) may cause problems with its state. You might for example remove some stored data that the element still needs.
If you want to preserve the element's state, you can do so by defining a
connectedMoveCallback()
lifecycle callback inside the element class, and then using the
Element.moveBefore()
method to move the element (instead of similar methods such as
Node.insertBefore()
). This causes the
connectedMoveCallback()
to run instead of
connectedCallback()
and
disconnectedCallback()
You could add an empty
connectedMoveCallback()
to stop the other two callbacks running, or include some custom logic to handle the move:
js
class MyComponent {
// ...
connectedMoveCallback() {
console.log("Custom move-handling logic here.");
// ...
Registering a custom element
To make a custom element available in a page, call the
define()
method of
Window.customElements
The
define()
method takes the following arguments:
name
The name of the element. This must start with a lowercase letter, contain a hyphen, and satisfy certain other rules listed in the specification's
definition of a valid name
constructor
The custom element's constructor function.
options
Only included for customized built-in elements, this is an object containing a single property
extends
, which is a string naming the built-in element to extend.
For example, this code registers the
WordCount
customized built-in element:
js
customElements.define("word-count", WordCount, { extends: "p" });
This code registers the
PopupInfo
autonomous custom element:
js
customElements.define("popup-info", PopupInfo);
Using a custom element
Once you've defined and registered a custom element, you can use it in your code.
To use a customized built-in element, use the built-in element but with the custom name as the value of the
is
attribute:
html
To use an autonomous custom element, use the custom name just like a built-in HTML element:
html
Scoped custom element registries
The examples above register custom elements on the global
CustomElementRegistry
accessed via
Window.customElements
. This means every custom element name you register must be globally unique across the entire page. As applications grow and begin combining components from multiple libraries, global name collisions can become a problem — if two libraries both try to define
, one of them will fail.
Scoped custom element registries
solve this by letting you create an independent registry whose definitions only apply to a specific DOM subtree, such as a
ShadowRoot
. Different shadow trees can each use their own registry with their own definitions, even if the element names overlap.
Creating a scoped registry
Create a scoped registry using the
CustomElementRegistry()
constructor and register elements on it with
define()
, just like the global registry:
js
const myRegistry = new CustomElementRegistry();
myRegistry.define(
"my-element",
class extends HTMLElement {
connectedCallback() {
this.textContent = "Hello from scoped registry!";
},
);
Note:
Scoped registries do not support the
extends
option in
define()
(for creating
customized built-in elements
). Attempting to use
extends
with a scoped registry throws a
NotSupportedError
DOMException
Associating a scoped registry with a shadow root
One way to use a scoped registry is to pass it to
Element.attachShadow()
via the
customElementRegistry
option. Elements parsed or created inside that shadow tree will then use the scoped registry's definitions instead of the global one:
js
const host = document.createElement("div");
document.body.appendChild(host);
const shadow = host.attachShadow({
mode: "open",
customElementRegistry: myRegistry,
});
//
shadow.innerHTML = "
You can also associate a scoped registry after the shadow root has been created by calling
initialize()
. This is useful when you need to set up the DOM structure first and attach the registry later:
js
const shadow = host.attachShadow({
mode: "open",
customElementRegistry: null, // no registry yet
});
shadow.innerHTML = "
// Later, associate the scoped registry and upgrade elements
myRegistry.initialize(shadow);
Declarative shadow DOM with scoped registry
For
declarative shadow DOM
, you can use the
shadowrootcustomelementregistry
attribute on a
element. This tells the HTML parser to leave the shadow root's
customElementRegistry
as
null
, so a scoped registry can be attached later with
initialize()
html
Responding to attribute changes
Like built-in elements, custom elements can use HTML attributes to configure the element's behavior. To use attributes effectively, an element has to be able to respond to changes in an attribute's value. To do this, a custom element needs to add the following members to the class that implements the custom element:
A static property named
observedAttributes
. This must be an array containing the names of all attributes for which the element needs change notifications.
An implementation of the
attributeChangedCallback()
lifecycle callback.
The
attributeChangedCallback()
callback is then called whenever an attribute whose name is listed in the element's
observedAttributes
property is added, modified, removed, or replaced.
The callback is passed three arguments:
The name of the attribute which changed.
The attribute's old value.
The attribute's new value.
For example, this autonomous element will observe a
size
attribute, and log the old and new values when they change:
js
// Create a class for the element
class MyCustomElement extends HTMLElement {
static observedAttributes = ["size"];
constructor() {
super();
attributeChangedCallback(name, oldValue, newValue) {
console.log(
`Attribute ${name} has changed from ${oldValue} to ${newValue}.`,
);
customElements.define("my-custom-element", MyCustomElement);
Note that if the element's HTML declaration includes an observed attribute, then
attributeChangedCallback()
will be called after the attribute is initialized, when the element's declaration is parsed for the first time. So in the following example,
attributeChangedCallback()
will be called when the DOM is parsed, even if the attribute is never changed again:
html
For a complete example showing the use of
attributeChangedCallback()
, see
Lifecycle callbacks
in this page.
Custom states and custom state pseudo-class CSS selectors
Built in HTML elements can have different
states
, such as "hover", "disabled", and "read only".
Some of these states can be set as attributes using HTML or JavaScript, while others are internal, and cannot.
Whether external or internal, commonly these states have corresponding CSS
pseudo-classes
that can be used to select and style the element when it is in a particular state.
Autonomous custom elements (but not elements based on built-in elements) also allow you to define states and select against them using the
:state()
pseudo-class function.
The code below shows how this works using the example of an autonomous custom element that has an internal state
"collapsed"
The
collapsed
state is represented as a boolean property (with setter and getter methods) that is not visible outside of the element.
To make this state selectable in CSS the custom element first calls
HTMLElement.attachInternals()
in its constructor in order to attach an
ElementInternals
object, which in turn provides access to a
CustomStateSet
through the
ElementInternals.states
property.
The setter for the (internal) collapsed state adds the
identifier
hidden
to the
CustomStateSet
when the state is
true
, and removes it when the state is
false
The identifier is just a string: in this case we called it
hidden
, but we could have just as easily called it
collapsed
js
class MyCustomElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
get collapsed() {
return this._internals.states.has("hidden");
set collapsed(flag) {
if (flag) {
// Existence of identifier corresponds to "true"
this._internals.states.add("hidden");
} else {
// Absence of identifier corresponds to "false"
this._internals.states.delete("hidden");
// Register the custom element
customElements.define("my-custom-element", MyCustomElement);
We can use the identifier added to the custom element's
CustomStateSet
this._internals.states
) for matching the element's custom state.
This is matched by passing the identifier to the
:state()
pseudo-class.
For example, below we select on the
hidden
state being true (and hence the element's
collapsed
state) using the
:hidden
selector, and remove the border.
css
my-custom-element {
border: dashed red;
my-custom-element:state(hidden) {
border: none;
The
:state()
pseudo-class can also be used within the
:host()
pseudo-class function to match a custom state
within a custom element's shadow DOM
. Additionally, the
:state()
pseudo-class can be used after the
::part()
pseudo-element to match the
shadow parts
of a custom element that is in a particular state.
There are several live examples in
CustomStateSet
showing how this works.
Examples
In the rest of this guide we'll look at a few example custom elements. You can find the source for all these examples, and more, in the
web-components-examples
repository, and you can see them all live at
An autonomous custom element
First, we'll look at an autonomous custom element. The
custom element takes an image icon and a text string as attributes, and embeds the icon into the page. When the icon is focused, it displays the text in a pop up information box to provide further in-context information.
See the example running live
See the source code
To begin with, the JavaScript file defines a class called
PopupInfo
, which extends the
HTMLElement
class.
js
// Create a class for the element
class PopupInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
connectedCallback() {
// Create a shadow root
const shadow = this.attachShadow({ mode: "open" });
// Create spans
const wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
const icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
const info = document.createElement("span");
info.setAttribute("class", "info");
// Take attribute content and put it inside the info span
const text = this.getAttribute("data-text");
info.textContent = text;
// Insert icon
let imgUrl;
if (this.hasAttribute("img")) {
imgUrl = this.getAttribute("img");
} else {
imgUrl = "img/default.png";
const img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);
// Create some CSS to apply to the shadow dom
const style = document.createElement("style");
console.log(style.isConnected);
style.textContent = `
.wrapper {
position: relative;
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
img {
width: 1.2rem;
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
`;
// Attach the created elements to the shadow dom
shadow.appendChild(style);
console.log(style.isConnected);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
The class definition contains the
constructor()
for the class, which always starts by calling
super()
so that the correct prototype chain is established.
Inside the method
connectedCallback()
, we define all the functionality the element will have when the element is connected to the DOM. In this case we attach a shadow root to the custom element, use some DOM manipulation to create the element's internal shadow DOM structure — which is then attached to the shadow root — and finally attach some CSS to the shadow root to style it. We don't do this work in the constructor because an element's attributes are unavailable until it is connected to the DOM.
Finally, we register our custom element in the
CustomElementRegistry
using the
define()
method we mentioned earlier — in the parameters we specify the element name, and then the class name that defines its functionality:
js
customElements.define("popup-info", PopupInfo);
It is now available to use on our page. Over in our HTML, we use it like so:
html
data-text="Your card validation code (CVC)
is an extra security feature — it is the last 3 or 4 numbers on the
back of your card.">
Referencing external styles
In the above example we apply styles to the shadow DOM using a