Let’s talk about web components

Web components! They’re currently at the vanguard of web development and are a reliable source of hot drama in the community. We’ve built a number of web component-powered design systems with Fortune 500 companies over the last 4 years, and it’s been a wild ride. We’ve seen a ton of success, we’ve bumped up against many rough edges, and we’re still here. We are increasingly relying on web components to help our clients establish and evolve their design system efforts. We’re enthusiastic about web components and think you should be too.

Web components: the basic gist

Web components are a collection of technologies that work together to deliver reusable UI components to the web. I won’t really delve into the technical details (if you want to get nitty gritty, check out Dave’s web components course), but the spirit is to be able to create and use a web component like this:

<product-card
  imgSrc="path/to/img.jpg" 
  imgAlt="T-shirt illustration of two Daft Punk helmets..."
  heading="Daft Punk T-Shirt"
  description="Turn up da funk with this stylish robot t-shirt"
  href="/product-url">
</product-card>

That spits out something that looks like this:

A card UI component featuring a daft punk helmet t-shirt, a title of "Daft Punk T-Shirt", a description that reads "Turn up da funk with this stylish robot t-shirt", and a blue button that reads "Shop Now"

The product-card web component abstracts away the product card’s front-end markup, styles, and any presentational JavaScript and delivers it as a nice little package. Consuming developers interact with the web component’s API in order to pass in content and/or turn functionality on or off.

Because web components are native to the web, they can travel to any web-based environment. They can be plugged into a WordPress/Drupal/Contentful/whatever site, integrated into a React/Vue/Angular/Svelte/whatever app, or sent to a regular ol’ static website. That’s the theory anyways! Anyone who’s worked with web components knows reality is a bit more complicated than that, and we’ll get into those things in a bit.

Web components really shine for design systems

There are many use cases for web components, but I’m going to focus on web components as a vehicle for delivering design system component libraries. It’s increasingly common for organizations to have web stuff built in a whole slew of technologies, and at the end of the day we want to deliver high-quality, cohesive user experiences to all of them. Users don’t give a crap if the homepage is powered by Drupal and the post-login dashboard is a React app. They want to get things done and get on with their lives, and our software should help them accomplish their goals with minimum fuss and muss. Design systems are an important tool for accomplishing that.

I see it critically important to differentiate between front-of-the-front-end development and back-of-the-front-end development. Crafting solid front-of-the-front-end code — HTML, CSS, and presentational JavaScript — is critical for delivering great user experiences, and it makes sense to avoid re-creating front-of-front-end code for each (past, present, and future) tech stack under an organization’s roof. That’s why web components are an attractive solution for a big challenge! Web components can serve as a vehicle for delivering a directly consumable “source of truth” for front-of-the-front-end code that can travel to whatever tech stack needs to be supported.

Web components vs [JS library/framework] is a false dichotomy

  • Web components vs React
  • Web components vs Angular
  • Web components vs Vue
  • Web components vs Svelte
  • Web components vs [whatever]

Web components versus [JS library/framework]” is a positioning that I hear a lot in my own work, and is a sentiment I see bandied about in the community. This is both a false dichotomy and a damn shame.

React! Angular! Vue! There are some massively popular JS libraries and frameworks out there, and they have proven to be incredibly capable solutions for delivering websites and apps the world over. There are giant ecosystems and communities built up around them, and a lot of collective labor has gone into getting them to the state they’re in today. People should be proud of all that hard-earned success.

There are many facets that go into creating great web experiences, which is why in our work it’s less “either/or” and more “both/and” when it comes to using web components and JS libraries/frameworks.

As mentioned above, it increasingly makes sense to deliver presentational UI components in a way that can be used across many different tech stacks. But there’s a lot more to making web experiences than the presentational UI layer. Business logic, state management, cache invalidation, et al all need to be handled, and these vast JS ecosystems are certainly up to the task. We’ve found a lot of success with a one-two punch that looks like this:

A venn diagram, where on the left a circle with text that reads "Web components: front-of-the-front-end, markup, CSS, presentational JS" partially overlaps with another circle that reads "JS libraries: back-of-the-front-end: logic, data, routing, etc"

  • Web components handle front-of-the-front-end code (e.g. the look and feel of a button)
  • JS libraries/frameworks handle back-of-the-front-end code (e.g. what happens when a user clicks on that button)

Web components are compatible with and can be consumed by JS libraries and frameworks like React, Angular, Vue, and the like. Web components handle the presentational aspects of a component, but are intentionally dumb. (Note: I’m specifically talking about design system web components; you can create very sophisticated, smart web components too). A JS library/framework can consume these dumb presentational web components and breathe life into them in order to make them work in a real product. In our work, we’ve found this split creates a clear separation of concerns and leads to a healthy division of labor.

There is (or should be) nuance around (how we talk about) web component rendering

Here’s the truth: the overwhelming majority of web components don’t need to be web components. Your run-of-the-mill, meat-and-potatoes UI components — the stuff of design systems — aren’t doing quadruple backflips or traveling to the moon. They’re more likely splatting a blue button on a web page.

This runs us right into one of the most common and valid criticisms of web components: why the hell do we need JavaScript at all in order to render web components? That’s a damn good question, and is one that my progressive enhancement-minded self has asked many times over the years. The partial answer has to do with the fact that web components were born during a different, SPA-frenzied zeitgeist. Setting that history aside, let’s dig into the nuance of web components rendering and talk about how we can successfully marry web components with progressive enhancement, server-side rendering, and the current zeitgeist.

Basic, static components

For a (strong?) majority of UI components, we want the developer ergonomics of working with the web component abstraction, which again looks something like this:

<ds-badge
  variant="success" 
  text="99 Luftballons"
>
</ds-badge>

But we want the browser to spit out something that looks like this:

<div class="badge badge--success">
  99 Luftballons
</div>

That, my friends, is called HyperText Markup Language, and it’s how the web has been working since 1991. This specific component doesn’t have any JavaScript razzle-dazzle going on, so effectively we want the web components to build in the same way we’ve been doing forever with templating languages like Mustache, Twig, Nunjucks, Jade, JSP, et al.

Alas, the design of web components relies on JavaScript in order to print the component out (irrespective of whether or not the component actually contains any JS-reliant behavior). The result looks like this in the browser (the right side of the image):

Web components rely on JS to render even a static UI component. If JavaScript isn’t available, the component won’t render.

Well that ain’t good. What do we do with this? Throw web components out the window? Hold yer horses there, pal. There are different solutions to this problem, so let’s first look at the design of HTML and how we can use that to our benefit.

Leaning into HTML’s design

When HTML doesn’t recognize a tag, it will proceed to render anything that happens to live inside of it:

The fault-tolerant nature of HTML is something web components can lean into in order to provide a progressively-enhanced experience

Hey, now we’re onto something! If/when JavaScript kicks in, the web component can wipe out or transform its guts into something more interactive. Let’s look at the idea of “spicy sections“, which the OpenUI crew have been working on in order to hopefully get us native tabs/accordion behavior into HTML proper:

<spicy-sections>
  <h2>Header</h2>
  <p>Content</p>

  <h2>Header</h2>
  <p>Content</p>

  <h2>Header</h2>
  <p>Content</p>
</spicy-sections>

When the web component kicks in, the headings get converted into tabs or accordion handles, depending on the available space. Say, that’s a nice bit of progressive enhancement! Users get access to the content whether or not JS is available, but receive an optimized experience when JS kicks in. This makes my heart circa 2006 sing. We’ll talk about some of the weirdness of this pattern in a bit, but first let’s introduce another thing that builds on this feature of HTML.

Declarative Shadow DOM

Declarative Shadow DOM is a technology that’s a part of the web component spec that sprinkles a few extra elements/concepts into this nested-HTML pattern to give more control to the web component. Here’s an example taken from this article:

<demo-greeter name="World">
  <template shadowroot="open">
    <style>
      b { color: red; }
    </style>
    Hello <b>World</b>!
  </template>
</demo-greeter>

Declarative Shadow DOM uses the template HTML tag (with accompanying shadowroot="open" attribute) in order to provide markup or styles on page load, ensuring users have access to necessary content and styles regardless of JavaScript status. When JavaScript kicks in, the component gets hydrated and becomes interactive. So alright! We have the best of both worlds: initial content that transforms into an enhanced experience. Progressive enhancement FTW!

So here’s the thing: this looks ugly as crap. And while we’re at it, the “take raw markup in a certain configuration and convert it to something interactive when JS kicks in” pattern is clever, but it’s a black box that’s ripe for abuse and you need to know how the internals work in order to wield it properly. In short, the developer experience isn’t good and I don’t think anyone’s out there promoting this as a preferred way of interacting with web components. The question then becomes: can we get the progressive enhancement benefits of declarative shadow DOM along with a great developer experience?

The best of both worlds

To reiterate, we want the following:

  • A developer experience of writing abstracted custom elements like <my-badge variant="success" text="99 Luftballoons">
  • A user experience of rendering initial markup/styles that then get progressively enhanced when JS becomes available.

We can accomplish both of these goals by employing a build step. And that’s what solutions like lit-ssr, WebC, and Enhance are helping to accomplish. Developers can write this:

<ds-badge
  variant="success" 
  text="99 Luftballons"
>
</ds-badge>

And it will compile into raw HTML like this:

<div class="badge badge--success">
  99 Luftballons
</div>

Or a declarative shadow DOM version like this:

<ds-badge variant="success" text="99 Luftballoons">
  <template shadowroot="open">
    <style>...</style>
    <div class="badge badge--success">
      99 Luftballoons
    </div>
  </template>
</ds-badge>

There’s even more nuance to all of this that I’m not going to go into here. Instead, I’d recommend checking out Zach Leatherman‘s WebC Eleventy docs, especially these videos that get into the various ways to transform a web component into the appropriate thing.

Right now, everyone’s clamoring for SSR support in web components. And since you’re a couple thousand words into this fandango, I’ll be so bold as to share one prediction and one hope:

  • My prediction: This time next year there will be largely settled solutions for this stuff. It’s being actively being worked on by many smart people and I reckon this time next year there will be mature patterns, better browser support, better tooling, more participants, and better understanding of what exactly “server-side rendering web components” means.
  • My hope: My pessimistic brain thinks that “SSR” will become another dumb checkbox alongside “accessibility” (which of course should be handled with much more consideration than ticking a box). My optimistic brain thinks this current zeitgeist is an opportunity to rekindle healthy conversations about progressive enhancement and helps a new generation of web creators (re)discover the promise of the world wide web: to empower humanity by connecting the world — regardless of a person’s browser/device, geography, network speed, capabilities etc. If you want some inspiration in this arena, you can do no better than to check out the great Jeremy Keith and his many talks and writing on progressive enhancement.

Shadow DOM is good, bad, and ugly

When training our clients on web components, we see our fair share of raised eyebrows. There are certain new concepts that are fundamentally different than how other frameworks and web stuff work. A big part of that comes from this idea of the Shadow DOM.

The idea behind Shadow DOM is that a component is completely self contained, which prevents a component’s styles or JavaScript from bleeding out onto the rest of a web page and knocking over a bunch of chairs. Zach Leatherman reminded me that the design of Shadow DOM was likely taking into heavy consideration things like third-party widgets and advertisements, which have always been a source of consternation for web developers around the world.

But design system work entails creating a component library often comprised of ~40-150-ish components that are all meant to hang together as one happy, interoperable family. For design systems, each web component operating as a totally island makes things clumsy or difficult at times. Making things like compound components look and behave appropriately (e.g. a data table with zebra striping, a select menu with different dropdown options) is challenging, and the hoops we’ve jumped through to get things to work don’t have us feeling great.

Moreover, Shadow DOM unlocks several different capabilities, including scoped styles and slots. So while you can turn ShadowRoot off to get around certain annoyances, you lose some of web components’ superpowers like scoped styles and slots. The good news is that there are efforts underway to treat each superpower as its own thing so we have more control over which web component features we want to lean into.

Lit, Stencil, and other tools

I sometimes see commentary floating around out there about how web components are “just JavaScript” and we actually don’t need frameworks or tooling to make them happen. These people would also like to talk your ears off about the merits of rolling your own cigarettes.

The fact of the matter is that the world of web development enjoys — nay, needs — abstractions, tooling, conventions, and parameters. I see tooling and frameworks as a healthy and important aspect of getting people rolling with web components. Other popular JS libraries/frameworks have built up some downright sturdy ecosystems and tooling, and I think it’s a good thing for web components to follow suit.

There are a few popular web component libraries/frameworks out there, and Lit and Stencil are two of the most popular. We’ve built design systems for some of the largest companies in the world using both of them, and they’re both good. I won’t get into a tools comparison here (you can Google it and find good articles like this), but holding my finger up to the wind (well, ducking my head into lots of companies) it seems like Lit is pulling away as the frontrunner.

But just so it’s said, the popularity of these tools have so little to do with the actual merits of the tools themselves (they all make making web components easy), and have way more to do with mindshare, branding, corporate backing, stars, monthly downloads, number of blog posts, and so on. In my consulting work, we help our clients choose the best technical solutions for the snapshot in time we’re working in, but we also help them architect things in such a way that today’s tooling doesn’t paint them into a corner when the tooling landscape inevitably changes.

A multi-faceted JS library/framework landscape

I want to touch on proprietary JS libraries/frameworks for a second. jQuery was a really important and pioneering tool of its time: it smoothed out a lot of cross-browser headaches, introduced many important concepts, and made development a hell of a lot easier. Ultimately, many of those concepts and ergonomics made their way into JavaScript proper and the world is better off for it. There was an elegance and dignity to its decline and death; it’s job here was done and could ride off into the sunset.

I see proprietary libraries/frameworks like React in the same light. React pioneered many important concepts — especially introducing an elegant component model — and those concepts have now made their way into the fabric of the web. But unlike jQuery, I don’t predict or welcome a death of React, Vue, Angular, or any JS framework. After all, JavaScript “don got big” and now powers the entirety of a web apps. This will undoubtedly continue to be the case.

My proposal is for these JS library/framework ecosystems to actively support & encourage web component use for front-of-the-front-end code, while continuing to handle all the mission-critical things that go into making web sites/apps work. I think the more HTML, CSS, and presentational JavaScript is handled by the web platform, the better off we’ll be. Reading things like “Why We’re Breaking Up with CSS-in-JS” is case in point. Let’s work with the grain of web as much as we’re able; it will benefit us all.

Web components are part of the web. They are good for the web. We should be rooting for them.

All these years later, I still believe in the web and its power to change the world for the better (Granted I’m not nearly as bright-eyed, bushy-tailed, naive, and optimistic as I once was). I come from the Zeldman school of web standards, am a strong proponent of progressive enhancement, care deeply about accessibility, and want to do my part to make sure that the web lives up to its ideals. And I bet you feel similar.

It’s in that spirit that I want to see web components succeed. Because web components are a part of the web! Any web component shortcomings that exist today certainly aren’t insurmountable in my view; I see them more as annoying wrinkles than anything more fundamentally flawed. So let’s work together, smooth out some wrinkles, save ourselves a lot of agony, and get us all to a better place.