Web Components are pretty amazing. They offer powerful features straight from the platform, some of the headliners are; true encapsulation of both JavaScript and CSS, cross-framework interoperability and of course a standardised component model for easily reusable UI components.

There’s another compelling feature of Web Components which hasn’t got as much attention, mostly down to the fact it’s not available natively in many browsers yet and some browser vendors have even said they won’t implement it at all — customized built-in elements.

What are Customized Built-in Elements?

Customized built-in elements which are part of the Custom Elements specification allow developers to extend a native element, adopt that elements functionality and also add entirely new capabilities.

An elementary example would be, imagine you wanted a user to confirm navigation every time they clicked on a link. To do this, you could extend the native anchor element ( <a> ), intercept the click and use a confirm prompt to ask the user if they were sure. It would look something like this:

If you’re already familiar with Custom Elements, you’ll notice we are doing a few things differently when we define our Custom Element.

class ConfirmLink extends HTMLAnchorElement {

...

} customElements.define("confirm-link", ConfirmLink, {

extends: "a"

});

We extend HTMLAnchorElement instead of HTMLElement because this is the interface the native anchor ( <a> ) element implements. Then, when we call the define() method on the Custom Elements API, we add a third argument which specifies an object with an extends property. Here we specify the tag name of the element we wish to extend.

Hang on, why do we need to specify the tag name of the element if we’ve just extended the HTMLAnchorElement interface? Shouldn’t that be enough to infer we are extending the anchor element? Unfortunately, some elements such as quote ( <q> ) and blockquote ( <blockquote> ) share the same interface ( HTMLQuoteElement ) so specifying the interface alone isn’t enough — leaving us to specify this seemingly redundant third argument to the define method.

After this, it’s pretty much business as usual. In the connected callback, we add an event listener to intercept the click on the host element. When this happens we use the confirm dialog to ask the user to confirm. Depending on the response we then either prevent navigation with e.preventDefault() or do nothing, therefore allow navigation.

connectedCallback() { this.addEventListener("click", e => { const result = confirm(

`Are you sure you want to go to '${this.href}'?`

); if (!result) e.preventDefault(); }); }

Then all we need to do to use this new functionality is add the is attribute to a native anchor element like so:

<a href="https://thewebplatformpodcast.com" is="confirm-link"></a>

The value for the is attribute is the tag name we chose for our Custom Element, this anchor element will then inherit the functionality we’ve just added.

Why do we need the capability to directly extend built-in elements?

The ability to instantly inherit built-in features of native elements has some pretty obvious benefits. It would take a lot of work (and a lot of code) to recreate many of the features provided by these elements. Imagine having to deal with keyboard interactions, focus behavior, the subtleties of how change events work, making an element work properly within a form, the list goes on and on.

But you don’t need directly extend a built-in element to inherit it’s features or reproduce the same functionality with the anchor element above. You could just wrap the native element within a normal (autonomous) Custom Element like this:

With autonomous Custom Elements we extend the HTMLElement interface and skip the third argument to the define() method.

class ConfirmLink extends HTMLElement {

...

}

customElements.define("confirm-link", ConfirmLink);

And instead of using a native anchor element we just use our Custom Element.

<confirm-link href="https://thewebplatformpodcast.com">Web Platform Podcast</confirm-link>

In the constructor of the class in this example I’m creating a Shadow Root for the Custom Element with the attachShadow() method.

this.attachShadow({ mode: "open" });

This allows me to use the Shadow DOMs slotting feature to project the inner content of the Custom Element directly into the wrapped anchor element using the slot element.

<a href="${href}">

<slot></slot>

</a>

Text node gets projected into the slow within the anchor element

This prevents us from having to manually move the Custom Elements innerHTML into the anchor element and also takes care of any future DOM updates.

I then proxy the href attribute of the Custom Element on to the anchor element.

static get observedAttributes() { return ["href"]; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (this._$a === null) return; this._$a.setAttribute("href", newValue); } }

And finally I add the click interception functionality.