An Attempted Taxonomy of Web Components
As my experience with web components grows, my personal view of how to build a web component is also evolving. In some cases I’ve gone back and refactored older components with the new knowledge and experience I’ve gained. In other cases, these older components sit as evidence of the learning path I traveled.
For the record, I mean zero-dependency web components that do not use an upstream library—they inherit directly from HTMLElement
or similar platform classes.
I thought it might be useful to catalog my journey of these open source web components as breadcrumbs for others.
HTML Web Components
These components layer on interactivity and add behaviors in true progressive enhancement fashion. This is the bread and butter use case of web components. They work great as-is and are unlikely to be improved with additional leverage of server-side rendering. All of the client-side DOM modifications are in service of a particular client-side behavior.
Tag | Size | CSS | JavaScript |
---|---|---|---|
details-utils |
3.6 kB |
None | Event Listeners only. |
video-radio-star |
2.4 kB |
None | Event Listeners only. |
announcement-banner |
1.1 kB |
Manual. Separate CSS styles are required to prevent layout shift. | Event Listeners only. |
is-land |
3.1 kB |
None. | Unknown custom elements are renamed for lazy initialization. |
squirm-inal |
3.0 kB |
JS injected. CSS is global and only injected once. The CSS is only necessary for the JS enhanced experience. | Child content is removed and re-added incrementally. |
resize-asaurus |
1.4 kB |
JS injected, scoped to Shadow DOM. CSS is only necessary for JS enhanced experience. | Size element added as overlay, no layout shift. |
Obligatory nod to :defined
—an incredibly useful tool for styling HTML Web Components.
The following components augment/modify nested HTML. These components could be improved with a tighter coupling to server rendering (e.g. WebC) but work great as-is in low-JavaScript environments.
Tag | Size | CSS | JavaScript |
---|---|---|---|
ppp-price |
2.9 kB |
None. | Updates text with adjusted price. |
seven-minute-tabs |
3.0 kB |
None. | Updates elements with accessibility mapping, hides non-active tabs. |
filter-container |
3.4 kB |
Optionally JS injected. CSS content is specific to each instance of the component. Can be added manually (if server rendering). |
Updates hidden, filtered elements (pairs nicely with server-rendering). |
table-saw |
2.2 kB |
JS injected. CSS content is specific to media/container query breakpoint config. | Updates table cells to add header text. |
browser-window |
2.9 kB |
JS injected, scoped to Shadow DOM. | Adds browser chrome. |
There is also a third category of HTML web component that augments the HTML in a way that is dynamic and specific to an individual user agent. A component like this wouldn’t be improved with a tighter coupling to a server (or cannot due to hosting limitations). I haven’t open sourced one of these yet.
As an example, an Intl.DateTimeFormat
wrapper web component that localize datetimes is floating around in one of my projects somewhere. This type of component could be server rendered in an edge function but this would limit the component’s hosting portability (and many edge function implementations are teetering on venture capital jenga towers of funding).
All of the components above use progressive enhancement and fallback to the nested HTML content before/without JavaScript—the crux of the humble HTML Web Component.
The Very Short and Recent History of HTML Web Components
The community has settled on this name very quickly, ok.
- Naming party on Mastodon (October 10, 2023)
- HTML web components—Jeremy Keith (November 9, 2023)
- HTML Web Components—Jim Nielsen (November 13, 2023)
- HTML Web Components are Just JavaScript?—Miriam Eric Suzanne (November 15, 2023)
- HTML Web Components—Chris Ferdinandi (November 16, 2023)
JavaScript Web Components
For the sake of completeness, I will sheepishly admit that I have created non-HTML Web Components too (and gotten value out of those). These are lower-priority optional use cases that pair with existing content and though it’s not ideal—for these use cases I’ve made the trade off.
Tag | Size | CSS | JavaScript Content |
---|---|---|---|
speedlify-score |
2.9 kB |
JS injected, scoped to Shadow DOM. | Entirely JS generated content (no fallback). |
infinity-burger |
1.4 kB |
JS injected, scoped to Shadow DOM. | Entirely JS generated content (no fallback). |
For example, <speedlify-score>
(used extensively on the Eleventy Leaderboards) is entirely JS-generated and has an empty fallback experience.
Lessons Learned
I usually gauge the quality of a web component based on the amount of JavaScript DOM modifications that occur in that component. It’s no surprise to learn that I prefer HTML Web Components and typically try to avoid using and authoring JavaScript Web Components.
Injecting CSS with Javascript
I was and am wary of this generally but there is additional nuance to keep in mind when comparing this to the much-maligned CSS-in-JS approach. The JavaScript injected CSS happening here is at the stylesheet level using the CSSStyleSheet API.
That is, one stylesheet is injected globally for all instances of the custom element (unless otherwise specified). <table-saw>
is one exception to that, but the stylesheet is de-duplicated if more than one instance uses the same breakpoint and media/container query type.
The jury is still out on how expensive Shadow DOM constructable stylesheets are. The Eleventy Leaderboards are a pretty good stress test of that currently: <speedlify-score>
uses Shadow DOM with a constructable stylesheet and currently has 918 component instances on the page (though lazy initialized via <is-land>
).
The part I do like about injecting stylesheets is that I don’t need to worry about distributing my CSS separate from the JavaScript—it feels like a web-platform single file component. This is a fine tradeoff to make when the component’s CSS is tightly coupled and in service to the JavaScript of the component. Injecting CSS with JS will not get you anywhere if you need to style the before/without JavaScript (fallback) experience, so buyer beware!
Nuances of Shadow DOM
If you generate content and markup with JavaScript—it may be okay to put it in Shadow DOM and get the benefits of scoped styles and devtools inspector-collapsed markup. Make sure you’re aware of the limitations of Shadow DOM (forms, deep hash links, accessibility mapping). Others have written extensively about this:
- Web Components Accessibility FAQ—Manuel Matuzović (September 2023)
- Pros and cons of using Shadow DOM and style encapsulation—Manuel Matuzović (August 2023)
- How Shadow DOM and accessibility are in conflict—Alice Boxhall (February 2023)
- Shadow DOM and accessibility: the trouble with ARIA—Nolan Lawson (November 2022)
- Dialogs and shadow DOM: can we make it accessible?—Nolan Lawson (June 2022)
- Accessibility and the Shadow DOM—Marcy Sutton (February 2014)
Some components can’t be web components (for now?)
Some components require server rendering. You couldn’t do a good enough <img>
or <picture>
wrapper component (e.g. Eleventy Image or Next.js’ <Image>
) using custom elements.
I suppose you might be able to attempt such a thing and limit it exclusively to loading="lazy"
images, but that seems a bit risky and fraught with peril. A custom element can’t beat the preload scanner and that markup is best served server-rendered.
It’s also worth noting that it isn’t possible to put a custom element into <head>
—the HTML parser doesn’t allow it. You can try this yourself.
<!-- some boilerplate markup omitted -->
<head>
<custom-element></custom-element>
</head>
is parsed as:
<body>
<custom-element></custom-element>
</body>
Conclusions
There is a lot of complexity in this space right now. But there is hope as the community refocuses on an approach with a good pit of success: the humble HTML Web Component. A very important takeaway here is that creating an HTML Web Component doesn’t require a hard stance of Light or Shadow DOM in your implementation (though Shadow DOM still has dragons). It is only concerned with the before/after JavaScript, with web performance and durability considerations in mind.
It will require discipline to avoid overindexing on JavaScript-injected CSS. I think <browser-window>
may have jumped over the line here and I’m still a little uneasy about the tradeoffs I made there but the portability benefits are quite tempting!
Web components thrive in producing extremely portable and long-lasting code that can live and adapt to a huge variety of hosting and authoring environments. If you haven’t yet, give them a try!
2 Comments
bkardell
@zachleat > The CSS is only necessary for the JS enhanced experience.I am having trouble parsing this... Can you clarify?
Zach Leatherman
@bkardell As an example, for <squirm-inal> the CSS is related to the JavaScript-enhanced animation behavior: https://github.com/zachleat/squirminal/blob/8a0c5567c81f3082118d07f28c7228c51ec4db24/squirminal.js#L24-L49 (cursor blinking, etc)The fallback is the default slot HTM… Truncated