Zach’s ugly mug (his face) Zach Leatherman

The Good, The Bad, The Web Components

Silhouette of people riding horses on brown field during daytime.
January 31, 2024 #5 Popular

This post was created from a talk. You can watch this in video form at JSHeroes 2023.

The humble component. The building block of modern web development.

// MyButton.jsx
function MyButton() {
  return (
    <button>I'm a button</button>
  );
}

// Usage
<MyButton/>

You may recognize the above example taken from the documentation of one of the most popular component libraries in use today—IndieWeb Avatar for https://vercel.com/IndieWeb Avatar for https://react.dev/Vercel.js.

Folks may not know that the web platform has some component functionality built-in and is evolving support for new component development standards and specifications moving forward! These features are broadly known as Web Components.

Aside from Vercel.js, there are a variety of other popular component libraries too (of varying degree of web component friendliness):

Library Uses Native
Web Components
Custom Elements
as Compile Target
Compatibility
Score
IndieWeb Avatar for https://alpinejs.dev/Alpine No No Unknown
IndieWeb Avatar for https://angularjs.org/Angular No Yes 100%
IndieWeb Avatar for https://emberjs.com/Ember No No Unknown
IndieWeb Avatar for https://lit.dev/Lit Yes Yes 100%
IndieWeb Avatar for https://preactjs.com/Preact No Yes 100%
IndieWeb Avatar for https://qwik.builder.io/Qwik No No Unknown
IndieWeb Avatar for https://react.dev/React No No 67%
IndieWeb Avatar for https://www.solidjs.comSolid No Yes 94%
IndieWeb Avatar for https://stenciljs.com/Stencil Yes Yes 100%
IndieWeb Avatar for https://svelte.dev/Svelte No Yes 94%
IndieWeb Avatar for https://vuejs.org/Vue No Yes 100%

Compatibility score data from Custom Elements Everywhere, a test suite for web component compatibility.

Web Components are already a Success Story

Despite some notable criticism, web components are already widely used across the web.

In August 2023, Chrome Platform Status reports that 19.4% of page loads in Google Chrome were using a web component (via the CustomElementRegistryDefine key). For comparison <img loading> was at 15% and CSS Grid at 20%.

You can check out more social proof on: Are Web Components A Thing Yet?

They are particularly popular in large enterprise design system implementations:

Slide 19Slide 20Slide 21Slide 22Slide 23Slide 24Slide 25Slide 26

Many of the component libraries listed above (even those written by authors that have criticized web components) have an export-to-web-component feature built-in.

But—what are they?

Web Components is an umbrella term that encompasses many web platform technologies and we’ll go over a couple of the big (and in my opinion, most relevant) ones.

Custom Elements

Slide 35Slide 36Slide 37Slide 38Slide 39Slide 40Slide 41Slide 42Slide 43Slide 44Slide 45Slide 46Slide 47Slide 48Slide 49

Custom Elements allow you to attach a JavaScript class to any custom element you define on your page, to add behaviors and JavaScript-generated content to any element instances on the page. These are automatically initialized for you for the full lifecycle of the page (for server-rendered, JavaScript-generated, or content injected via fetch()).

  1. Custom elements cannot be void elements (like <img> or <meta>): they must have a start and end tag.
  2. Custom elements must have a dash in the tag name, so as to not conflict with future additions to the web platform.
Slide 61

If you populate these with your own server-rendered nested HTML (also known as the default slot/Light DOM/plain ’ol HTML) and use Custom Elements to for behavior-only additions (and not modify rendered DOM), you can even avoid layout shift!

Some folks have started referring to this approach as creating HTML Web Components (see post for examples).

If your expectation is full stack component-driven development, you will quickly encounter a developer experience problem here when authoring components in this way. Multiple instances of the same component need to repeat the same nested content and any changes to nested content need to be applied manually to all instances. You end up writing this:

<my-counter><button>1</button></my-counter>
<my-counter><button>2</button></my-counter>
<my-counter><button>3</button></my-counter>

When we want to author this (and have it server-render as the above):

<my-counter>1</my-counter>
<my-counter>2</my-counter>
<my-counter>3</my-counter>

Shadow DOM

Slide 68Slide 69Slide 70Slide 71Slide 72Slide 73Slide 74Slide 75Slide 76Slide 77Slide 78Slide 79Slide 80Slide 81Slide 82Slide 83Slide 85

Shadow DOM is the next level of our web components evolution. It solves the developer experience problem with repetition in authoring markup at the expense of clientside rendering 😭.

This trade-off introduces additional complexities around managing layout shift and the flash of unstyled content (FOUC). Typical approaches to solve these problems either put all of the JavaScript into the critical rendering path (in the <head>, which I try to avoid for performance reasons) and/or hide the components until they’re defined via JavaScript (which is by definition a performance problem for critical content).

One such approach to assert control over pre-definition and post-definition styling is the CSS pseudo-class :defined. It’s a very useful tool for styling and progressive enhancement (occupying some of the same architectural vibes as the scripting media query), but in Shadow DOM heavy component libraries it is often applied to hide components while loading (👎🏻 no thank you).

Slide 86Slide 87Slide 88Slide 89Slide 90Slide 91Slide 92

Declarative Shadow DOM

Slide 94Slide 95Slide 96Slide 97Slide 99Slide 102Slide 103Slide 104Slide 105Slide 106Slide 107Slide 108

As we evolve to our next level of web components, we move up to Declarative Shadow DOM (sometimes known as DSD). This allows you to put your Shadow DOM template in nested markup inside of each element instance and the browser will create a shadowRoot for you with the template contents (no JavaScript required).

This solves the clientside rendering dependency for Shadow DOM but at the expense of repetition in authoring markup! The ol’ switcheroo (in some ways) feels like a de-evolution back to the approach we discussed in Custom Elements!

Uniquely, this approach does allow you to use scoped CSS (and <slot>) without a JavaScript dependency. Non-declarative (imperative/JavaScript) Shadow DOM offers scoped CSS too but repeats some of the mistakes made by CSS-in-JS approaches and introduces a runtime JavaScript dependency on styling.

<my-counter>
  <template shadowrootmode="open">
    <style>
    * { color: blue; }
    </style>
    <button><slot></slot></button>
  </template>
</my-counter>

Again, the repetition of these templates in every component instance is tedious and brittle.

We can see the tension between authoring repetition and server-side rendering, right? (Non-declarative) Shadow DOM was the only approach so far that offered a good authoring experience without repetition, but required clientside rendering.

Server-side Rendering

If we could have a reusable Declarative Shadow DOM template that did not require nesting, that would be ideal, right? (Discussion on this topic is happening in this WICG issue)

I would love something like this (though I acknowledge the likely issues with streaming here):

<!-- ⚠️ THIS CODE IS ASPIRATIONAL -->
<!-- ⚠️ IT DOESN’T WORK ANYWHERE -->
<template definition="my-counter">
  <style>
  button {}
  </style>
  <button><slot></slot></button>
</template>

<my-counter>1</my-counter>
<my-counter>2</my-counter>
<my-counter>3</my-counter>

This is why folks will claim that server-side rendering is yet to be a solved problem—it has not yet been solved at the web platform level. I’d also wager that this is the biggest remaining complaint about web components.

Folks expect this to be solved because this is a problem that many component libraries have solved. However, I would warn that we’re holding these two disparate things to different expectations.

Clientside component frameworks can and should be compared to web component specifications and tools. But if your component framework introduces an additional server rendering step or abstraction, it seems unfair to compare that to clientside-only web components.

Full stack server-rendered Svelte or React cannot and should not be compared to clientside web component specifications like Custom Elements or Shadow DOM—these are apples and oranges. The biggest thing I hear from these criticisms is that we need an additional server-rendered abstraction for web components, too.

Framework Tension

Slide 128Slide 129Slide 130Slide 132Slide 133

JavaScript Frameworks and Web Components can work together—though some are more web component friendly than others.

In some respects it feels like the web platform chased the early clientside rendered, single page application (SPA) architectural vibes of React, Ember, Angular, and others. The more nimble frameworks have pivoted away from those mistakes. The platform will always be a bit behind those that live further down the stack (and importantly, informed by their work) but with the right amount of patience can offer improved performance and long-term maintenance potential.

  • If you want to go all-in on Web Components, have a look at these answers to the server-rendered web component question:
  • React has the worst custom-elements-everywhere.com compatibility test score. If you’re deep in React-world, look at using IndieWeb Avatar for https://preactjs.com/Preact to provide a more future-friendly and compatible experience.
  • Solid, Angular, Svelte, and Vue support web components as a compilation target too.

A WebC Example

I built WebC so I guess I can now pivot this post to show you a small example of the rendered output you get when using it.

Consider a content template file index.webc:

<my-counter>1</my-counter>
<my-counter>2</my-counter>

Classical HTML

Next, we’ll add a component definition for my-counter to _components/my-counter.webc:

<style webc:scoped>
:host button {}
</style>
<button><slot></slot></button>

WebC will render index.webc as:

<style>
  .wrfp4zhxg button {}
</style>

<my-counter class="wrfp4zhxg">
  <button>1</button>
</my-counter>
<my-counter class="wrfp4zhxg">
  <button>2</button>
</my-counter>

Declarative Shadow DOM

You can also modify your component definition (_components/my-counter.webc) to use Declarative Shadow DOM, avoiding the issue of repeating your Shadow DOM template in your content:

<template shadowrootmode="open">
  <style>
  button {}
  </style>
  <button><slot webc:raw></slot></button>
</template>
<slot></slot>

WebC will now server-render index.webc as:

<my-counter>
  <template shadowrootmode="open">
    <style>
    button {}
    </style>
    <button><slot></slot></button>
  </template>
  1
</my-counter>

<my-counter>
  <template shadowrootmode="open">
    <style>
    button {}
    </style>
    <button><slot></slot></button>
  </template>
  2
</my-counter>

This server-applied abstraction offers a variety of benefits over earlier approaches:

Slide 138Slide 139Slide 140Slide 141Slide 142Slide 143

If you want to learn more, you can try out the eleventy-base-webc starter project or for a deeper dive into progressive enhancement strategies have a look at Seven Progressive Enhancement Recipes using Eleventy WebC Image Comparison Components.

Appendices

This talk was given at four different events in 2023 (some of which videos are available):


IndieWeb Avatar for https://unsplash.com/Poster image by Yusuf Onuk

< Newer
carouscroll Web Component
Older >
Join the 11ty International Symposium on Making Web Sites Real Good (it’s an 11ty Conference)

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is 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 »

22 Reposts

IndieWeb Avatar for https://www.alvinashcraft.commichelDave ???? :cursor_pointer:Pär BjörklundHolger HellingerCory :prami_pride_demi:Jason GarberDoug Parker ????️DerekHasan AliBob MonsourRichard MacManusAram Zucker-ScharffRyan TownsendKevin C. TofelJean Pierre KolbNick Tune ????????Dan JacobRepeter wants an Ukr victorynrk 9819Garrow Bregenza :lattentacle:Khalid ⚡

35 Likes

MayankKhalid ⚡michelPär BjörklundDave ???? :cursor_pointer:Holger HellingerSerhiy BarhamonAlistair ShepherdJon Maskrey*tbesedaNathanDerekAdam StoddardHasan AliBaldur Bjarnasoncthos ????Bruno B. de SouzaJames ScholesstffndkAshur CabreraAram Zucker-ScharffJack WellbornHeather BuchelEric McCormickPiper HaywoodPeterJean Pierre KolbNick Tune ????????Dan JacobJay Contoniomeduz'nrk 9819Charlie ParkJeff
17 Comments
  1. Khalid ⚡

    Khalid ⚡

    @zachleat The “h-i” slide cracks me up. ????

  2. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @khalidabuhakmeh we do what we must ????

  3. Khalid ⚡

    Khalid ⚡

    @zachleat Great post. I’d like to see the aspirational idea you have come to fruition. The biggest issue with web component approaches, even the good ones like WebC, is that they still require a build step. This limits them to folks who are comfortable with JS build chains.I want… Truncated

  4. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @khalidabuhakmeh 100% agree—and I’m hopeful we’ll get there!

  5. Simon MacDonald

    Simon MacDonald

    @khalidabuhakmeh @zachleat the aspirational ideal is literally how Enhance works. You define your template in a function and then it is expanded into light DOM custom elements by the SSR piece.We’d love to bring Enhance SSR to other languages so if you are interested in submittin… Truncated

  6. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @macdonst @khalidabuhakmeh ah to be clear I mean that I want the aspirational idea to be a first-party platform feature.

  7. Matt Wilcox

    Matt Wilcox

    @khalidabuhakmeh Won't lie... it's what keeps me from investigating them any further than "the theory". I don't want node/npm on a live server. I have and trust PHP/Composer. Mostly because those are already my dependencies, and I don't want new ones with … Truncated

  8. nrk 9819

    nrk 9819

    @zachleat Great write up as always. I've been following your blog and @blog for a while for web components related articles.Also Vercel.js was funny (how we reached from Facebook.js to this point lmao ????????????)

  9. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @mattwilcox @khalidabuhakmeh sure, but the repetition issue can be “poorly” hacked together via other mechanisms in your server-rendered stack: https://www.zachleat.com/web/webc-in-eleventy/#a-history-of-component-like-things (PHP include comes to mind)But, yeah. It’s interesting… Truncated

  10. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @nrk9819 thank you! ????

  11. Simon MacDonald

    Simon MacDonald

    @zachleat @khalidabuhakmeh Same! I think we need to rally around declarative custom elements since it is too late to change the web component apis.

  12. Khalid ⚡

    Khalid ⚡

    @macdonst @zachleat I work with #dotnet and I don't expect MS to give folks a build-step that would allow you to process HTML before the final artifacts are built any time soon :(There is a way to possibly do it, but I think it wouldn't be ideal. dotnet

  13. Simon MacDonald

    Simon MacDonald

    @khalidabuhakmeh Is there no render step on the server you can tap into?

  14. tomwor

    tomwor

    @zachleat Vercel.js - love it. ????

  15. Hasan Ali

    Hasan Ali

    @zachleat Loved the talk when I first saw it and I’m really glad there’s a written version of it ????????

  16. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @hasanhaja it only took me 7 or 8 months to get it on the web site ???? (thank you!)

  17. Mayank

    Mayank

    @zachleat yea totally. declarative custom elements will need way more functionality, but we can start small

Shamelessly plug your related post

These are webmentions via the IndieWeb and webmention.io.

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)