Join the AI Workshop and learn to build real-world apps with AI. A hands-on, practical program to level up your skills.
Custom Elements can be used to build rich, reusable UI. For example, the CSS Doodle library uses them to create CSS-based animations. This tutorial explores how Custom Elements work under the hood.
Custom Elements let you define new HTML tags.
It may not be obvious why this is useful until you see it in action—we already have many built-in tags. Libraries and apps use Custom Elements to encapsulate behavior and styling in reusable components.
This tutorial covers Custom Elements v1, the current version of the standard.
Using Custom Elements we can create a custom HTML tag with associated CSS and JavaScript.
It’s not an alternative to frameworks like React, Angular or Vue, but it’s a whole new concept.
The window global object exposes a customElements property that gives us access to a CustomElementRegistry object.
The CustomElementRegistry object
This object has several methods to register Custom Elements and look up ones already registered:
define()— defines a new Custom Elementget()— returns the constructor of a Custom Element (orundefinedif not defined)upgrade()— upgrades an existing element to a custom elementwhenDefined()— returns a promise that resolves with the constructor when the element is defined; similar toget()but asynchronous
How to create a custom element
Before we can call the window.customElements.define() method, we must define a new HTML element by creating a new class that extends the HTMLElement built-in class:
class CustomTitle extends HTMLElement {
//...
}
Inside the class constructor we’re going to use Shadow DOM to associate custom CSS, JavaScript and HTML to our new tag.
In this way, all we’ll see in the HTML is our tag, but this will encapsulate a lot of functionality.
We start by initializing the constructor:
class CustomTitle extends HTMLElement {
constructor() {
super()
//...
}
}
Then we call the attachShadow() method on the element, passing an object with the mode property set to 'open'. This property sets the encapsulation mode for the Shadow DOM. If it is open, you can access the element’s shadowRoot. If it is closed, you cannot.
Here’s how to do so:
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
//...
}
}
Some examples use const shadowRoot = this.attachShadow(/* ... */), but you can skip storing it unless you set mode to 'closed', since you can always access the shadow root via this.shadowRoot.
Which is what we’re going to do now, to set the innerHTML of it:
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<h1>My Custom Title!</h1>
`
}
}
You can add as many tags as you want, you’re not limited to one tag inside the
innerHTMLproperty
Now we add this newly defined element to window.customElements:
window.customElements.define('custom-title', CustomTitle)
and we can use the <custom-title></custom-title> Custom Element in the page!
Note: you cannot use self-closing tags;
<custom-title />is not allowed by the standard.
Notice the hyphen (-) in the tag name. A Custom Element name must contain a hyphen. That is how the browser distinguishes custom elements from built-in ones.
Now we have this element in the page, and we can do what we do with other tags: target it with CSS and JavaScript!
Provide a custom CSS for the element
In the constructor, you can pass a style tag in addition to the HTML tag that defines the content, and inside that you can have the Custom Element CSS:
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<style>
h1 {
font-size: 7rem;
color: #000;
font-family: Helvetica;
text-align: center;
}
</style>
<h1>My Custom Title!</h1>
`
}
}
Here’s the example Custom Element we created, in Codepen: https://codepen.io/flaviocopes/pen/LKgjzK/
A shorter syntax
Instead of first defining the class and then calling
window.customElements.define() we can also use this shorthand syntax to define the class inline:
window.customElements.define('custom-title', class extends HTMLElement {
constructor() {
...
}
})
Add JavaScript
As with CSS, you can attach JavaScript to the element.
You cannot add scripts inside the template the same way as CSS; attach listeners in the constructor instead.
This example adds a click listener in the Custom Element constructor:
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<h1>My Custom Title!</h1>
`
this.addEventListener('click', (e) => {
alert('clicked!')
})
}
}
Alternative: use templates
Instead of defining the HTML and CSS in a JavaScript string, you can use a template tag in HTML and assign it an id:
<template id="custom-title-template">
<style>
h1 {
font-size: 7rem;
color: #000;
font-family: Helvetica;
text-align: center;
}
</style>
<h1>My Custom Title!</h1>
</template>
<custom-title></custom-title>
Then you can reference it in your Custom Element constructor and add it to the Shadow DOM:
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const tmpl = document.querySelector('#custom-title-template')
this.shadowRoot.appendChild(tmpl.content.cloneNode(true))
}
}
window.customElements.define('custom-title', CustomTitle)
Example on Codepen: https://codepen.io/flaviocopes/pen/oramEY/
Lifecycle hooks
In addition to constructor, a Custom Element class can define those special methods that are executed at special times in the element lifecycle:
connectedCallbackwhen the element is inserted into the DOMdisconnectedCallbackwhen the element is removed from the DOMattributeChangedCallbackwhen an observed attribute changed, or is added or removedadoptedCallbackwhen the element has been moved to a new document
class CustomTitle extends HTMLElement {
constructor() {
...
}
connectedCallback() {
...
}
disconnectedCallback() {
...
}
attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}
attributeChangedCallback() receives 3 parameters:
- the attribute name
- the old value of the attribute
- the new value of the attribute.
The callback runs only for observed attributes. You declare those in an array returned by the static observedAttributes getter:
class CustomTitle extends HTMLElement {
constructor() {
...
}
static get observedAttributes() {
return ['disabled']
}
attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}
With disabled in the list, when the attribute changes—for example when you run:
document.querySelector('custom-title').disabled = true
—attributeChangedCallback() runs with the arguments 'disabled', false, and true.
Note: the browser may call
attributeChangedCallback()in other situations; do not invoke it yourself. Let the browser invoke it when attributes change.
Define custom attributes
You can define custom attributes for your Custom Elements by adding a getter and setter for them:
class CustomTitle extends HTMLElement {
static get observedAttributes() {
return ['mycoolattribute']
}
get mycoolattribute() {
return this.getAttribute('mycoolattribute')
}
set mycoolattribute(value) {
this.setAttribute('mycoolattribute', value)
}
}
This is how you can define boolean attributes, ones that are “true” if present, like disabled for HTML elements:
class CustomTitle extends HTMLElement {
static get observedAttributes() {
return ['booleanattribute']
}
get booleanattribute() {
return this.hasAttribute('booleanattribute')
}
set booleanattribute(value) {
if (value) {
this.setAttribute('booleanattribute', '')
} else {
this.removeAttribute('booleanattribute')
}
}
}
How to style a Custom Element that’s not yet defined
JavaScript may load after the initial paint, so a Custom Element might not be defined when the page first loads. The layout can shift when the element is eventually defined and rendered.
To avoid this, use the :not(:defined) pseudo-class to reserve space and fade in the element once it is defined:
custom-title:not(:defined) {
display: block;
height: 400px;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
Can I use them in all browsers?
Current versions of Firefox, Safari, Chrome, and Edge support Custom Elements. Internet Explorer does not and will not support them.
You can use this polyfill to add support for older browsers as well.
Lessons in this unit:
| 0: | Introduction |
| 1: | ▶︎ Custom Elements |