Use web components for what they’re good at

Web components logo of two wrenches together

Dave Rupert recently made a bit of a stir with his post “If Web Components are so great, why am I not using them?”. I’ve been working with web components for a few years now, so I thought I’d weigh in on this.

At the risk of giving the most senior-engineer-y “It depends” answer ever: I think web components have strengths and weaknesses, and you have to understand the tradeoffs before deciding when to use them. So let’s explore some cases where web components really shine, before moving on to where they might fall flat.

Client-rendered leaf components

To me, this is the most unambiguously slam-dunk use case for web components. You have some component at the leaf of the DOM tree, it doesn’t need to be rendered server-side, and it doesn’t <slot> any content inside of it. Examples include: a rich text editor, a calendar widget, a color picker, etc.

At this point, you’ve already bypassed a bunch of tricky bits of web components, such as Server-Side Rendering (SSR), hydration, slotting, maybe even shadow DOM. If you’re not using a framework, or you’re using one that supports web components, you can just plop the <fancy-component> tag into your template or JSX and call it a day.

For instance, take my emoji-picker-element. It’s one line of HTML to import it:

<script type="module" href="https://cdn.jsdelivr.net/npm/emoji-picker-element@1/index.js"></script>

And one line to use it:

<emoji-picker></emoji-picker>

No bundler, no transpiler, no framework integration, just copy-paste. It’s almost like ye olde days of jQuery plugins. And yet, I’ve also seen it used in complex SPA projects – web components can run the gamut.

This is about as close as you can get to the original vision for web components, which is that using <fancy-element> should be as easy as using built-in HTML elements.

Glue code, or avoiding the big rewrite

Picture this: you’ve got a big React codebase, it’s served you well for a while, but now your team wants to move to Svelte. Time to rewrite the whole thing, right? Including finding new Svelte versions of every third-party component you’re using?

This is the way a lot of frontend devs think about frameworks, with all these huge switching costs when moving from one to the other. The biggest misconception I’ve seen about web components is that they’re just another flavor of the same story.

They’re not. The whole point of web components is to liberate us from this churn. If you decide to switch from Vue to Lit, or from Angular to Stencil (or whatever), and if you’re rewriting all your components in one go, then you’re signing up for a lot of unnecessary pain.

Just let your old code and your new code live side-by-side. Use web components as the interoperability later to glue the two together. You don’t need to rewrite everything all at once:

<old-component>
  <new-component>
  </new-component>
</old-component>

Web components can pass props/attributes down, and send events back up. (That’s kind of their whole thing.) If your framework supports web components, then this works out of the box. (And if not, you can write some lite glue code.)

Now, some people get squeamish at the idea of two frameworks living on the same page, but I think this is more gut-based than evidence-based. And to be fair, if you’re using a meta-framework to do SSR/hydration, then this partial migration may be easier said than done. But if web components are good at anything, it’s defining a high-level contract for composing two components together, on the client side anyway.

So if you’re tired of rewriting your whole UI every year (or your boss is tired of you doing it), then maybe web components are worth considering.

Design systems and enterprise

If you watch Cassondra Robert’s talk from CSS Day, there’s a nice slide with a lot of corporate logos attesting to web components’ penetration:

CSS Day talk screenshot showing Cassondra Roberts alongside a slide showing company logos of Adobe, RedHat, Microsoft, IBM, Google, Apple, ING, GitHub, Netlify, Salesforce, GitLab

If this isn’t enough, you could also look at Oracle, SAP, ServiceNow… the list goes on and on.

What you’ll notice is that a lot of big companies (like the one I work for) are quietly using web components, especially in their design systems and component libraries. If you spend a lot of time on webdev social media, this might surprise you. It might also surprise you to learn that, by some measures, React is used on roughly 8% of page loads, whereas web components are used on 20%.

The thing is, a lot of big companies are not on social media (Twitter/X, Reddit, etc.) trying to sell you on web components or teach you how to use them. On the other hand, there are plenty of tech influencers on Twitter busily keeping up to date with every minor version of React and what’s new in that ecosystem. The reason for this is pretty simple: big companies tend to talk a lot internally, but not so much externally, whereas small companies (agencies, startups, freelancers, etc.) tend to be more active on social media relative to their company size. So if web components are more popular inside the enterprise than outside of it, you’d never know it from browsing Twitter all day.

So why are big enterprises so gaga for web components? For one thing, design systems based on web components work across a variety of environments. A big company might have frontends written in React, Angular, Ember, and static HTML, and they all have to play nicely with the company’s theming and branding. The big rewrite (as described above) may be a fun exercise for your average startup, but it’s just not practical in the enterprise world.

Having a lot of consumers of your codebase, and having to think on longer timescales, just leads to different technical decisions. And to me, this points to the main reason enterprises love web components: stability and longevity.

Think about your average React codebase, and how updating any dependency (React Router, Redux, React itself, etc.) can lead to a weeks-long effort of rewriting your code to accommodate all the breaking changes. Cue the inevitable appearance of Hyrum’s Law at enterprise scale, where even a tiny change can cause a butterfly effect that breaks thousands of components, and even bumping a minor version can lead to weeks of testing, validating, and documenting. In this world, your average React minor version bump is an ordeal – a major version bump is a disaster.

Compare this to the backwards-compatibility guarantees of the web platform, where the venerable Space Jam website from 1996 still works to this day. Web components hook into this stability story, which is a huge plus for any company that doesn’t have the luxury of rewriting their frontend every couple years.

When you use a web component, connectedCallback is just connectedCallback – it’s not going to change. And shadow DOM style scoping, with all of its subtleties, is not going to change either. Whatever code you can delegate to the browser, that’s code you’re not having to maintain or validate over the years; you’ve effectively outsourced that responsibility to Google, Apple, and Mozilla.

Enterprises are slow, cautious, and risk-averse – just like the web platform. No wonder web components have taken the enterprise world by storm.

Downsides of web components

All of the pluses of web components should be weighed against their weaknesses. And web components have their fair share:

  • Server-side rendering (SSR). I would argue that this is still not a solved problem in web-components-land. Sure, we have Declarative Shadow DOM, but that’s just one part of the puzzle. There’s no standard for rendering web components on the server, so every framework does it a bit differently. The fact that Lit SSR is still under “Lit Labs” should tell you something. Maybe in the future, when you can render 3 different web component frameworks on the server, and they compose together and hydrate nicely, then I’ll consider this solved. But I think we’re a few years away from that, at least.
  • Accessibility. You can’t have ARIA references that easily cross shadow boundaries, and dialogs and focus can be tricky. At the very least, if you don’t want to mess up accessibility, then you have to think really carefully about your component structure from day one. There’s a lot of ongoing work to solve this, but I’d say it’s definitely rough out there in 2023.

Aside from that, there are also problems of lock-in (e.g. meta-frameworks, bundlers, test runners), the ongoing legacy of IE11 (some folks are scarred for life; the last thing they want to do is #useThePlatform), and overall platform exhaustion (“I learned React, it works, I don’t want to learn something else”). Not everyone is going to be sold on web components, and I’m fine with that. The web is a big tent, and everybody is using it for different things; that’s part of what makes it so amazing.

Conclusion

Use web components. Or don’t use them. Or come back and check in again in a few years, when the features and web standards are a bit more fleshed out.

I think web components are cool, but I understand that not everyone feels the same way. I don’t feel like I need to go around evangelizing for their adoption. They’re just another tool in the toolbelt; the trick is leveraging their strengths while avoiding their pitfalls.

The thing I like about web components, and web standards in general, is that I get to outsource a bunch of boring problems to the browser. How do I compose components? How do I scope styles? How do I pass data around? Who cares – just take whatever the browser gives you. That way, I can spend more time on the problems that actually matter to my end-users, like performance, accessibility, security, etc.

Too often, in web development, I feel like I’m wrestling with incidental complexity that has nothing to do with the actual problem at hand. I’m wrangling npm dependencies, or debugging my state manager, or trying to figure out why my test runner isn’t playing nicely with my linter. Some people really enjoy this kind of stuff, and I find myself getting sucked into it sometimes too. But I think ultimately it’s a kind of fake-work that feels good but doesn’t accomplish much, because your end-user doesn’t care if your bundler is up-to-date with your TypeScript transpiler.

That said, in 2023, choosing web components comes with its own baggage of incidental complexity, such as the aforementioned problems of SSR and accessibility. Compromising on either of those things could actively harm your end-users in ways that actually matter to them, so the tradeoff may not be worth it to you.

I think the tradeoff is often worth it, but again, there are nuances here. “Use web components for what they’re good at” isn’t catchy, but it’s a good way to think about it in 2023.

Thanks to Westbrook Johnson for feedback on a draft of this blog post.

11 responses to this post.

  1. Love your component, I tried it, and it works without any problem from NPM directly in my designer !
    (https://node-projects.github.io/web-component-designer-demo/index.html) nice work

    Reply

  2. Hi Nolan! Great post, keep them coming please :) One question: I don’t understand your argument around Declarative Shadow DOM — that the SSR is not solved. What do you mean by that? THe server only needs to ship the structure then the web component will be automatically upgraded (hydrated) on client. To me the how to do it on the server is a little irrelevant, I just need the structure arriving to the client. Here’s a little more detail on my point https://developer.chrome.com/en/articles/declarative-shadow-dom/

    Reply

    • True, there is a standard for what the output format of a web component on the server should look like. However, there’s no standard for the actual rendering itself. For a basic example, how should the server handle a component that dispatches events? Node.js has no Event object or a dispatchEvent method. Should the server polyfill it, or should Events be considered the same as APIs like getBoundingClientRect that don’t really make sense on the server?

      Or let’s say you have two components from two different frameworks, composed together, and the first framework encounters the tag other-component. How to render it? document.createElement("other-component")? There’s no document API on the server, and even if there were, there’s no way for the server to know which tag name maps to which framework, or which API method to use for it (e.g. render in Lit, createRenderer in Stencil, etc.).

      There’s some discussion in the Web Components Community Group about how to solve this, but AFAIK no two frameworks have agreed on anything yet.

      Reply

      • Really good points. I understand your view now much better. I still have a little challenge around understand why anything more needs to happen on the server beyond a simple html render. Some of the questions are valid indeed, like the two framework, but isn’t that a client rendering question? Does the client need anything more from the server than a standard html structure + the scripts needed to hydrate?

        Would love to continue this conversation somehow, I love it. Would you be open for a podcast chat on this?

      • It’s not so much about the HTML rendering; it’s about the framework interop. The problems are all solvable in theory, but unlike web components on the client, we don’t have a single standard that everyone can agree on.

        For hydration it may indeed “just work,” but that may depends on how the framework actually handles hydration. I know for instance that Lit has some logic to defer connectedCallback calls during hydration in some cases, but I don’t know if this is required functionality.

        I’m probably not the best person to talk about this on a podcast; I admit I’ve only begun thinking about the problem space. 😅

  3. Hey, I think they’re cool. And for precisely this reason:

    Use web components as the interoperability later to glue the two together.

    Composability! Hybridization like this works for larger companies not only for large or separate teams, but also: As a migration path for very large or old/legacy codebases.

    This is precisely how I’m using web components right now at eBay. For me it has quickly become an indispensable tool. I have an older PHP + jQuery/Sass based site that I’m slowly migrating over to Svelte. To do so, web components really fit the bill. I needed something that would check a lot of boxes for me (importantly, light DOM rendering with slot compatibility and Vite HMR to keep me sane). So, I wrote an NPM package called svelte-retag (which is a fork/rewrite of svelte-tag, see https://github.com/patricknelson/svelte-retag/) which helps my team to bridge the divide between our old 2014 jQuery based codebase and our new Svelte component based front-end code. The light DOM here is really important to help ease and iron out that transition from old to new without having to commit to a massive rewrite all at once. This also serves as an important stepping stone to a future were we may potentially be able to convert the entire front-end to SvelteKit (with a headless PHP-based CMS)

    The really awesome thing about web components here is that it works as a common language to normalize/serialize language of a component. This is extremely useful since, on the PHP side, I can generate/emit a component at any location as-needed, then render that client-side with ease. Not only that, but this offers some potential for SSR as well (especially if we’re working in the light DOM), even in hybrid PHP-heavy environments such as this. I haven’t had a chance to look at these yet, but check out enhance-ssr and webc if you haven’t seen them already. They seem to offer some options for SSR of web components (webc mentions “browser-native Shadow DOM style scoping” 🤷‍♂️).

    Another great example of how this has served us well was as a means for us to very efficiently and seamlessly implement a consistent header between our main corporate site and our jobs website (ebayinc.com and jobs.ebayinc.com). This isn’t Svelte, but rather a sort of encapsulated approach of allowing us to roll out our existing SSR-based header with our existing old jQuery/Sass based layout and functionality, but ensconced in the safety of the shadow DOM, away from from the outside interference of the host page (and vice versa; we don’t want to create regressions in their code as well)!

    That said, in 2023, choosing web components comes with its own baggage of incidental complexity, such as the aforementioned problems of SSR and accessibility. Compromising on either of those things could actively harm your end-users in ways that actually matter to them, so the tradeoff may not be worth it to you.

    And ultimately, that’s why for now I’m still sticking with the light DOM (where possible). For us, it offers these benefits and more, particularly for legacy reasons.

    Reply

    • That makes sense! I think ~80% of “web component problems” are actually “shadow DOM problems,” and so cutting out shadow DOM can avoid a lot of potential issues. Sadly though you do lose out on <slot>s, hence the light DOM “slots” you mention.

      Reply

  4. […] to the full podcast to hear more about Nolan’s views on web components, and read the accompanying blog post for more […]

    Reply

  5. Posted by Naresh Bhatia on November 13, 2023 at 8:48 AM

    Thank you for this excellent analysis, Nolan! I am curious if you have examples of “avoiding the big rewrite” and the approach taken. We are in a situation where we have an old codebase using React 16. We want to upgrade to React 18, however we have a huge complex module in the existing app which is tightly coupled using Redux state and communicating with external APIs. It’s not easy to break it into smaller components and wrap them individually into Web Components. One approach that we are thinking of is to wrap the entire module in a Web Component and let it live in a new React 18 app – until this module is rewritten in React 18. However, I don’t see anyone who has attempted to do this. Examples I have seen are small to medium size components with minimal state and self contained. Any thoughts on this approach or a better approach?

    Reply

    • With React you actually have another option, which is to first upgrade to React 17, which allows different parts of the tree to use different versions of React.

      Note I have never actually tried this myself, so I can’t comment on how well it will work! But if not then yes it sounds like decomposing sections of your app into self-contained web components would work as well. Unfortunately being tightly-coupled to a global store (Redux) will probably make this difficult to pull off.

      Reply

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.