Web components are not components

Let me preface this entire post by saying I love web components.

I’ve written this line of JavaScript maybe a thousand times by now, and I’m yet to tire of it.

customElements.define('bofa-button', class extends HTMLElement {

Once inside the opening brace, everything is scoped! I can grab elements with a simple this.querySelector('button'). I can add event handlers directly using this.addEventListener. It feels simple, it makes sense, and it’s a big improvement over working with “naked” DOM references.

“What’s bofa?”

Custom element names are shared in the global registry, so it’s generally a good idea to practice namespacing.

Let’s pretend we’re working for Bank of America. We can prefix all our custom elements with bofa- (e.g. <bofa-button>, <bofa-list>, <bofa-card>) to prevent any potential conflicts. The - requirement in the name seems like a restriction at first, but I’ve grown to like it because it pushes me to scope all my names.

That said, if you know for sure there will be no conflicts, the prefix can be avoided as long as the dash is still used. For example, <list-> and <card-> are perfectly valid custom elements.

Let’s forget about registering custom elements for a second. Even just <bofa-button> is pretty neat on its own. It gives me an easy selector to reference in CSS. And this selector has some advantages over regular class names, such as respecting :nth-of-type (basically of S selectors with better browser compatibility). Later, when I do need to add JavaScript, it’s ready to be registered at any time.

Cool, so what’s the problem? Does this blog post even need to exist?

At least Keith J Grant seems to think so. (Total coincidence on the post title 😅)

Problems with the web components APIs

Shadow DOM is not fit for production use

There’s no point in sugarcoating it — Shadow DOM has never been fit for most production use, and probably won’t be for a while.

You can’t even correctly select text inside shadow DOM in Firefox. Yikes!

Even if we forgive the bugs (it happens), it feels like shadow DOM was almost deliberately designed to be difficult. It gets in the way of styling and accessibility, both of which are absolute deal breakers for me. Just yesterday, Manuel Matuzović wrote a blog post covering some more scenarios where shadow DOM falls short.

With shadow DOM eliminated, we lose a lot of the power of web components, slots being the perhaps biggest thing that would be missed.

“But style scoping!”

I hear you. Shadow DOM comes with automatic style scoping. But it does so by throwing away the entire damn cascade, and that’s just not how CSS is supposed to work.

Besides, scoping is not a difficult problem to solve in user-land, and there’s native @scope coming to CSS which will make it simple to achieve the same thing.

@scope {
p { color: red; }
<p>only this becomes red</p>
<p>this is unaffected</p>

Boilerplate city

I showed off how cool custom elements are at the beginning of this post. There’s a reason I only showed the simplest case. Doing anything beyond that will take way more boilerplate code than it should.

Even accessing an attribute value as a property requires repeating the same boilerplate (because someone decided that making my life easier would be “problematic”). I often avoid using attributes altogether for this reason.

Inevitably, someone will see this and suggest that I “just use Lit” to reduce some of the boilerplate (in this case using reactive properties). This feels really strange to me, because suddenly I’m working with a third-party library that I have to lug around in every website, instead of working with what’s part of the platform (which was the whole point).

Besides, if I’m using a rendering framework anyway, why wouldn’t I choose one that’s simpler, smaller, easier to use, and avoids the pitfalls of shadow DOM? (I’ve personally used at least four that fit this criteria, and there are most definitely quite a few more.)

Not portable

We talked about custom elements, we talked about shadow DOM, now let’s talk about HTML imports. Oh right, they don’t exist. Instead, you need to use JavaScript to share any markup.

That means you need to wait for JavaScript to be loaded and processed before the markup becomes meaningful. I don’t know about you but that reminds me of the client-side rendered single-page applications of 2017, and I do not want live in that world.

You can make it slightly better by predefining the markup in <template> tags, however:

  1. You still need to populate the template somehow (probably using a rendering framework).
  2. The JavaScript still needs to run to make use of the template.

Declarative shadow DOM will help you get around the second problem, but then you’re back to dealing with the weirdness that comes with shadow DOM.

This is perhaps the crux of the whole thing. Web components require either client-side JavaScript or a rendering framework (or both) to be portable.

The perfect exception?

Maybe you need to build a fully isolated widget that should not be affected by the other CSS on the page, does not need to interact with any other elements on the page, is not critical for first render, does not contain multiple lines of text, does not need to be part of forms, is not complicated enough to run into accessibility issues, and can feasibly include a whole framework with its bundle.

Congratulations! You have found the perfect use case in which case web components can be used like components today.

What about design systems?

I often hear that web components are perfect for design systems. To that, I say: No*.

(Note the asterisk there.)

Custom elements that don’t incorporate shadow DOM (for reasons stated above) can indeed work well in design systems. But they will usually need to be wrapped within a framework component, whether that’s Preact, Svelte, Solid, Lit, Astro, WebC, what have you.

By making use of custom elements, you’ll get to use more of the platform and less of the framework. The framework exists as a convenience layer to fill the gaps of the platform (portability, server-side rendering, light DOM scoping, etc).

Closing thoughts

Web components are not bad (although shadow DOM specifically is). They’re actually neat. They’re also not components. They almost solve a different problem than framework components. And maybe that’s okay.

Web components

An animation that shows the word "web" followed by a loading spinner which eventually changes into the word "components".

Next up: What is a component anyway?