About Web Components

Andrea Giammarchi
10 min readAug 4, 2021
Photo by Anna Gru on Unsplash

It was the year 1998 when W3C proposed a way to gracefully enhance, via the CSS’ behavior property, any builtin element on the page that would inherit such behavior defined through one, or more, external .htc files.

<ul>
<li style="behavior:url(hilite.htc)">Example</li>
</ul>

Use Case / Abstract

One limiting factor (…) is that there is no way to formalize the services that an HTML application can provide, or to allow them to be reused as components in another HTML page or application. HTML Components address this shortcoming; an HTML Component (HTC for short) provides a mechanism for reusable encapsulation of a component implemented in HTML, stylesheets and script.

Componentization is a powerful paradigm that allows component users to build applications using ‘building blocks’ of functionality without having to implement those building blocks themselves, or necessarily understand how the building works in fine detail. This method makes building complex applications easier by breaking them down into more manageable chunks and allowing the building blocks to be reused in other applications.

… we’ll talk more about those parts in bold later on, bear with me!

Implementations

The first browser to implement and support this property, was already available in Windows XP and Windows Server 2003: Internet Explorer 5.

With HTC files, it was possible to:

  • attach events, something we’d probably do in a custom element’s constructor
  • define, and initialize, properties, something similar to observed attributes
  • define methods, something simple via classes definition
  • define some sort of accessor through GET and PUT directives

Everything was possible through a pretty ugly XML namespace, 3 context per script, but somehow a module like approach, without global variables leaking and so on, and this is an example you could test today in some IE8 VM.

The index.html content:

<!doctype html>
<style>
.behave { behavior: url(behave.htc); }
</style>
<ul>
<li class="behave">I behave!</li>
</ul>

The behave.htc content:

<PUBLIC:COMPONENT>
<PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="highlight.on()" />
<PUBLIC:ATTACH EVENT="onmouseout" ONEVENT="highlight.off()" />
<SCRIPT>
// this is each element dedicated behavior scope:
// nothing leaks in the global, nothing is shared.
var highlight = {
color: '',
letterSpacing: '',
on: function () {
this.color = runtimeStyle.color;
this.letterSpacing = runtimeStyle.letterSpacing;
runtimeStyle.color = 'red';
runtimeStyle.letterSpacing = 2;
},
off: function () {
runtimeStyle.color = this.color;
runtimeStyle.letterSpacing = this.letterSpacing;
}
};
</SCRIPT>
</PUBLIC:COMPONENT>

What’s most important to notice, is that nowhere, in this file, we need to describe the element name, or it’s builtin type, so that behaviors were already somehow superior to what we have today, because there’s zero global registry names conflicts, as long as the .htc files’ names won’t clash, and literally any element could implement one, or more, behaviors: ugly in the form, but gorgeous, and extremely powerful, in practice!

However, as we probably know by now, there are many Web standards that came from IE, and most of them have been rewritten and (hopefully) improved, over the years. But while one would expect that 23 years to make behaviors right, and use these in every modern browsers, should be enough, the current situation is instead the following one:

What happened?

This is a brief recap of the history of Web Components:

  • the initial version of the globally shared customElements registry was based on a V0 API that was never implemented in IE, legacy Edge, Firefox or Safari
  • since the beginning, it’s never been possible to declare more than a single behavior to a Custom Element, and it’s never been possible to define behaviors at runtime, specially if the custom element was not a builtin extend, as one cannot change a tag name on the fly
  • the newly proposed V1 API didn’t solve the registry clashing nature, didn’t improve what V0 was proposing, except for the class based syntax, admittedly more elegant, and it removed the createdCallback without providing a mechanism to know when the element, and its content, has been fully parsed (hence when it’s possible to inject nodes or text)
  • the current V1 API allows builtin extends, but one vendor decided not to implement the most demanded functionality out there for portability, graceful enhancement, and most importantly, the only part of the specs that doesn’t require to implement those building blocks themselves (buttons, tables, details, inputs, all things a11y, and so on)
  • because of the previous point, most developers mislead by polyfills that never implemented builtin extends, like the @ungap/custom-elements does instead, shipped most components through the Shadow DOM, which has been impossible to polyfill reliably, it’s still not compatible with Server Side Rendering, it solves none of the builtin extends features out of the box, it cannot be styled without injecting CSS per each component or attaching stylesheets at runtime via JS, it needs JS to even bootstrap a “button”, it doesn’t play well with options, tables rows, or cells, it’s slower and heavier than builtin components, something every modern library and framework offers these days anyway, it introduced the need for slots, an indirection of what builtin elements would represent out of the box, and nobody understood the use case, compared to what a fully cross-platform capable <iframe> element would do!

Why Shadow DOM?

The Web has historically abused <iframe> elements not just to somehow sandbox the shown content and grant its style, or its JS wouldn’t conflict with the hosting site, also to track users (see all social media buttons out there).

On top of that, each frame would likely require the network to show any content, and if the end point was unavailable, the page would’ve looked broken.

The Shadow DOM idea was to replace the “need for iframes” in the wild, granting that the style of its Shadow DOM would’ve been preserved, offering a class based mechanism, together with modules, to ensure no conflicts with the global code, and a confined sort of sandbox to enhance the page.

While the theory was good on paper, the practice is that only ads and ads providers somehow benefit from this technology, social media trackers are still widely used with iframes in most sites, browsers without Shadow DOM support needs heavy polyfill for something not even 100% reliable, on top of Custom Elements polyfill, and nothing works until all this JS is delivered.

What could’ve been different?

If the main reason to have Shadow DOM was to grant visual layout without conflicts or unexpected styles from the hosting page, we already had the reset-css approach, and it worked well, so that a new CSS property could’ve put an end to the debate, without any need to attach shadow roots all over the Web:

custom-element, .reset {
style-inherit: none !important;
}

… done?

About Custom Elements Builtin Extends

We can read in the official HTML Standard page that it is possible to extend builtin elements through the very same customElements registry.

<script type="module">
// class definition
class PlasticButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener("click", () => {
// Draw some fancy animation effects!
});
}
}
// custom element registration
customElements.define(
"plastic-button",
PlasticButton,
{extends: "button"}
);
</script>
<button is="plastic-button">Click Me!</button>

To simplify even further above dance, in a way that fixes styling issues and enables builtin extends out of the box, I’ve created vanilla-elements.

<script type="module">
import {define, HTML} from '//unpkg.com/vanilla-elements';
// define(name, Class) or soon!
@define('plastic-button')
class PlasticButton extends HTML.Button {
constructor() {
super();
this.addEventListener("click", () => {
// Draw some fancy animation effects!
});
}
}
</script>
<button is="plastic-button">Click Me!</button>

This module not only makes builtin extends, or even generic custom element, more elegant, it optionally includes, behind a proper feature detection, the tiny polyfill for Safari and WebKit based browsers, without affecting at all Chrome, Firefox, or Edge, as these all implement the native extend as spec’ed.

… but what happened to Safari?

The culprit, out of one of the longest discussions ever in the WHATWG repository, is this comment:

One fundamental problem is that subclassing a subclass of HTMLInputElement or HTMLImageElement often leads to a violation of the Liskov substitution principle over time. Because they're builtin elements, they're very likely to be extended in the future. Imagine that we had this feature supported before type=date was added. Then your subclass of HTMLInputElement must also support type=date in order to satisfy the substitution principle. Similarly, imagine we had added this feature before srcset was added to HTMLImageElement and you wrote a subclass of HTMLImageElement. Then not supporting srcset results in the violation of the principle again.

For humans, the concept is that builtin extends are risky, because the platform might decide to add an attribute that could break subclasses that maybe improperly used that attribute to date.

While this argument is technically valid, it doesn’t address a few things:

  • in 23 years we still don’t have Custom Elements fully implemented, or behaviors like IE5 had already, … I mean, it’s not that the DOM is famous for its speedy releases of anything, really, so time to migrate, from proposal to widely available feature, is usually months, if not years
  • if new attributes are the reason to not extend a builtin, any HTMLElement extend is also affected by the very same problem. Imagine the hidden attribute, or the contenteditable one wasn’t there already … and suddenly appears on the platform and “everyone is doomed” because they abused their ability to define any attribute they wanted, precisely like they should (carefully though) because that’s what Custom Elements offers as API

… moreover …

In addition, none of the builtin elements are designed to be subclassed, and in fact, we don’t have builtin subclasses of HTMLElement that are also builtins. This limits the usefulness of any sort of subclassing.

  • HTMLElement shares the same prototypal inheritance every other builtin uses and it is an Element builtin extend itself
  • the “usefulness limit” is exactly the reason developers want to subclass builtins: to avoid re-implementing on their side everything the builtin already offers: events, accessibility, focus behavior, style, and so on

Furthermore, we don’t have any hooks for the processing models and internal states of builtin elements

My thinking here is: if I can polyfill Safari through the platform native functionalities it provides, how is it possible that the WebKit team cannot provide this extend internally, where they control everything even more than I can do via JS?

One thing I agree though, is that element behaviors were, 23 years ago, a more flexible, mixin-like, approach to the problem, so it looks like we’ve spent a lot of years solving the wrong problem, and we’re not even there yet.

However, every argument made for builtin extends would still be valid for any new API that allows extending elements, because these will observe attributes, will be potentially future-hostile, and so on … let’s hope though.

… should we care about Safari?

Of course we should, and that is why I’ve created the polyfill that fixes WebKit and Safari only. It worked great for years and it doesn’t affect performance because Apple products already have the fastest CPUs on the planet, and 1 extra Kb of JS to fix the standard API is something no Apple product would suffer for, compared with the average mega bytes of JS they consume per each page, tab, iframe, site so … fear not, and builtin extends away, until there are better proposals that hopefully won’t take other 23 years in the making 😅

What Builtin Extends Solve

  • inherited native behavior, functionality, and accessibility out of the box
  • Server Side Rendering support out of the box: it’s just HTML after all, without fancy names that describe nothing, in terms of layout semantics
  • it’s graceful enhancement that works, like every other graceful enhancement library or framework worked to date
  • it requires less code without duplication needed to setup in Shadow DOM what was already in the container … so: it’s lighter for both Mobile and Desktop
  • it’s natively implemented in all browsers but one, which can be polyfilled if targeted, and doesn’t need to stop us, if we don’t target it (Electron apps come to mind, as well as intranets, IoT, and so on)
  • works with Tailwind, Bulma, or literally every other CSS framework out of the box, and work with pretty much every library too, allowing the building blocks to be reused in other applications

… and what don’t …

Meanwhile …

https://webcomponents.dev/

Most popular frameworks, libraries, and tools, used to build anything Web related these days, have a Component like approach that works, and it’s entirely based on builtins, without caring about Web Components, because at this point Web Components are too little, too late, and don’t really solve what these third party projects solved already for years:

  • in the server
  • in every client
  • without name clashing
  • and even in native applications

If V1 went out fully supported by everyone, maybe today everything would be based on builtin extends, would work faster, there would be less bloat, and so on. It’s easy to blame developers about tons of JS to show any basic page, but if the platform keeps lacking long-awaited features, blocking proposals everyone is excited about, or make APIs impractical to use, like Shadow DOM is in practice, we can’t just blame developers for having the utilities, or polyfills, they need, to make it work.

We all love the Web, but in stories like this one, it feels like the Web doesn’t really love us back … or not as much 💔

So thank you for reading and spreading the word: try builtin extends now, forget about Shadow DOM even if you use regular HTMLElement, and see how far you can go with less bloat, more native functionalities, and native Web development … and maybe, just maybe, one day Safari will work like others.

P.S. in case anyone is wondering “is there a good Web Components story?”, the answer is HTMLTemplateElement, which enabled a plethora of declarative libraries around the modern JS’ template string tag feature. That worked!

--

--

Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.