Customized Built-in Elements
📅
Lesson: Customized Built-in Elements
Why are they needed?
Until now, we've been creating autonomous custom elements, such as <time-formatted>. They are independent and don't carry any semantic meaning for the browser. Search engines don't understand their purpose, and assistive technologies (e.g., screen readers for the visually impaired) don't know how to work with them.
But what if we need to create a specialized button, an enhanced list, or an extended text field? It would be unwise to abandon all the built-in capabilities and semantics of standard HTML elements. This is exactly what customized built-in elements are for.
They allow inheriting functionality and behavior from built-in HTML elements (such as <button>, <input>, <ul>), adding new logic, styles, and capabilities to them. You create an element that "mixes" the power of a standard tag with your custom implementation.
How to create a customized built-in element
The process is similar to creating an autonomous element, but with several key differences.
1. Inherit from the built-in element class
Instead of inheriting from the base HTMLElement, your class should inherit from the class of the specific HTML element you want to extend, such as HTMLButtonElement, HTMLInputElement, HTMLAnchorElement, etc.
// Create a class for a button with animation
class PlasticButton extends HTMLButtonElement {
constructor() {
super(); // always call super() first
this.addEventListener('click', () => {
// Code for cool animation on click!
console.log('✨ Playing animation!');
});
}
// You can add your own methods and lifecycle hooks
connectedCallback() {
console.log('Plastic button added to the document!');
}
}
2. Register the element with tag specification
When registering an element with customElements.define(), we add a third argument - an object with the extends option. This is mandatory because many different tags (e.g., <q> and <blockquote>) use the same DOM interface (HTMLQuoteElement). The browser needs to know exactly which tag you're extending.
// Register the element, specifying that it extends the <button> tag
customElements.define('plastic-button', PlasticButton, { extends: 'button' });
3. Use the element in HTML
To use our new element, we don't create a new tag. Instead, we write a standard tag but add the is attribute to it, whose value is the name of your customized element.
Correct:
<button is="plastic-button">Click me!</button>
Incorrect:
<!-- This will just be a tag unknown to the browser without any semantics -->
<plastic-button>Don't do this</plastic-button>
Your button now retains all the standard properties and behavior of a regular button: it can receive focus, participate in form submission, be disabled using the disabled attribute, and the browser applies standard styles to it.
<button is="plastic-button" disabled>Disabled plastic button</button>
Creating an element via JavaScript
You can create a customized built-in element programmatically in two ways.
1. Using document.createElement():
For this, a special form of the method is used that takes a second argument - an object with the is option.
const plasticButton = document.createElement('button', { is: 'plastic-button' });
plasticButton.textContent = 'I was created in JS';
document.body.append(plasticButton);
2. Via class constructor:
You can create an instance directly by calling new YourClass().
const plasticButton2 = new PlasticButton();
console.log(plasticButton2.localName); // Will output "button" (base tag)
Interesting nuance: when created programmatically, the is attribute is not physically added to the DOM until the element is serialized (e.g., via outerHTML).
console.log(plasticButton.hasAttribute('is')); // false
console.log(plasticButton.outerHTML); // Will output '<button is="plastic-button">I was created in JS</button>'
Important limitations
Customized built-in elements can only extend modern, standard HTML elements (e.g., button, input, div, img). You cannot extend deprecated elements such as <bgsound>, <blink>, or <marquee>, since their class is HTMLUnknownElement.
This limitation ensures compatibility with future HTML versions. If developers could extend arbitrary tag names, it could block the implementation of new official elements with the same name in the future.
Summary
| Aspect | Autonomous element (e.g., <time-formatted>) |
Customized built-in element (e.g., <button is="plastic-button">) |
|---|---|---|
| Inheritance | class extends HTMLElement |
class extends HTMLButtonElement |
| Registration | define('my-element', MyElement) |
define('my-element', MyElement, {extends: 'button'}) |
| Semantics | No (new semantics are set manually) | Yes (inherited from the base element) |
| Usage | <my-element> |
<button is="my-element"> |
| Behavior | "Blank slate", you define everything | Inherits all behavior of the base element (focus, forms, styles) |
For creating complex UI components that need to be accessible and behave like standard elements (buttons, input fields, lists), it is always preferable to use custom built-in elements. Standalone elements are good for creating completely new, unique widgets that have no analogues in standard HTML.