Zach’s ugly mug (his face) Zach Leatherman

An Attempted Taxonomy of Web Components

A top-down photo of a large selection of open books
November 18, 2023 #2 Popular

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.

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:

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 -->

is parsed as:



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!

IndieWeb Avatar for image by Patrick Tomasso

< Newer
The Infinity Hamburger Menu, now in Web Component form
Older >
A New Technique for Image Optimization: SVG Short Circuiting

Zach Leatherman IndieWeb Avatar for a builder for the web and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 81 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »


IndieWeb Avatar for https://a.supportcontact.onlineIndieWeb Avatar for https://rssfeeds.cloudsite.buildersIndieWeb Avatar for https://apkhore.comIndieWeb Avatar for https://scottjehl.comIndieWeb Avatar for https://som2nypost.comIndieWeb Avatar for https://jakelazaroff.comIndieWeb Avatar for https://naildrivin5.comIndieWeb Avatar for https://dinezh.comIndieWeb Avatar for Avatar for https://soatdev.comIndieWeb Avatar for https://abuspeaks.comIndieWeb Avatar for https://techtoguide.comIndieWeb Avatar for https://moneymanages.comIndieWeb Avatar for https://siliconhype.comIndieWeb Avatar for https://swissvolcano.comIndieWeb Avatar for https://evolucioncreativa.websiteIndieWeb Avatar for https://washingtonread.comIndieWeb Avatar for https://digitalwebgeek.comIndieWeb Avatar for https://mycheapwebhosting.comIndieWeb Avatar for https://atocs.netIndieWeb Avatar for https://mastermindtechpro.comIndieWeb Avatar for https://cloudfour.comIndieWeb Avatar for https://derp.fooIndieWeb Avatar for https://zerobytes.monsterZach LeathermanMarcelle RusukatherineFabrice GanglerThe Spicy WebEvil Jim O’DonnellApple Annie :prami:Nick Tune ????????Noah Liebman


Ryuno-KiSheldon Chang ????????JeffJoe GaffeyFabrice GanglerSimon Cox :SEO:Paul MasonJakub Iwanowski :bash:SwiftRobin RendleEvil Jim O’DonnellAndy Daviesjalciné's job hunting but alsoDan JacobApple Annie :prami:Nick Tune ????????

1 Bookmark

IndieWeb Avatar for
  1. bkardell


    @zachleat > The CSS is only necessary for the JS enhanced experience.I am having trouble parsing this... Can you clarify?

  2. Zach Leatherman

    Zach Leatherman

    @bkardell As an example, for <squirm-inal> the CSS is related to the JavaScript-enhanced animation behavior: (cursor blinking, etc)The fallback is the default slot HTM… Truncated

Shamelessly plug your related post

These are webmentions via the IndieWeb and

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)