Skip to main content
Baldur Bjarnason

Liskov's Gun: The parallel evolution of React and Web Components

Baldur Bjarnason

Because this essay is over 11 000 words long(!) I’ve made a convenience EPUB file for offline reading. (EPUB only! No PDF this time.) You can download it over on the fulfilment service I use, Lemon Squeezy, with the option to pay what you want if you feel the urge to support my writing. Paying is absolutely optional.

Web dev keeps arguing about web components #

Unless you’re like me – an “extremely online” kind of web developer – you probably don’t notice the dramas and brouhahas that erupt in the web developer community with regularity. Most of the time these flame-wars offer little of value or less in terms of technical insight and the patter of angry voices fades away before it reaches the ears of the less online.

But a recent one, which began when the maintainer of the SolidJS web development framework wrote a long and very angry-sounding blog post outlining why he didn’t think web components represented the future of web development, is a little bit less usual. Not because it has staying power. It’s fading away at the same steady pace as these things usually do.

It’s interesting because it touches on what I think is a genuinely interesting technical issue, but in a way that pretty much forces every reader to ignore it.

Normally, these “debates” are free of useful information. Often it’s somebody railing against this or that for largely personal reasons – I’ve certainly been guilty of that. Other times the points being made on both sides are largely innocuous. The people involved just seem to be using their “outside voices”, metaphorically speaking, for no particular reason.

“You should write tests!”

“Yes, but you need to do it properly! Otherwise, they just get in the way!”

“I AGREE BUT I PHRASE IT IN A WAY THAT MAKES ME SOUND ANGRY AT YOU BECAUSE THIS IS THE INTERNET AND I’M PROBABLY ANGRY AT SOMETHING!”

It’s tiresome, especially because it’s common for this mode of debating to split people who are largely in agreement into groups that chalk each other up as assholes. The worst part being that both sides are generally true. Everybody involved had an asshole moment or two.

What makes the web components debate interesting is that, this being the internet, many of the participants are behaving like assholes (though not you, dear reader, you’re one of the wonderful sensible few of the internet) while at the same time making interesting yet conflicting technical points.

“These two groups seem to both be making solid technical points, and yet they end up disagreeing,” is always interesting. It means the topic has curious nuances and, possibly, unresolved hard questions.

That bit is genuinely fun, at least if you’re of a personality type that finds unresolved hard questions fun. The tone and attitudes of some of the posts involved make it difficult, but discovering if there’s a “there” there can be worthwhile.

But, because of the “everybody defaults to ‘asshole’” nature of social media, we often end up in a discourse archipelago where each island vehemently sticks to its opinions, never listening to the other, because most humans will resolutely ignore advice and observations if they come from somebody they consider to be an asshole.

The core point of the current hubbub, if you dig through the rhetoric and follow the links back to earlier discussions and arguments is:

It’s really hard to add extensibility and “composability” to an already complex class inheritance tree with pre-existing and rigidly defined composition system without making some hard compromises.

But to get to that point, the point where we can dig into the technical details of the whys and wherefores, we first need to understand the context.

We need to be able to look past the assholes to see the forest.

We need to understand what somebody means when they say “web components”.

WTF are Web Components? #

If you aren’t directly involved in specific kinds of web development, you might not have a clear idea of what Web Components are.

They are a set of web APIs that have been standardised and, for the most part, implemented by web browsers. They are a standard method for implementing and distributing reusable widgets.

That’s pretty much it. There are a few features and details that surround these, help them integrate with other APIs, help smooth the edges, but this is Web Components.

Together, these APIs let us build custom widgets that can be safely packaged into a custom element and will interoperate with pretty much anything that renders to a DOM, even React (though React specifically blocked custom elements for what honestly sounded like spurious reasons for the longest time and even now full support has only shipped in a beta release).

Web Components aren’t what you would call a “disruptive” innovation, but they are genuinely useful. More so now they more reliably work with forms in modern browsers. They are a “hey, you know that thing browsers do? Well now you can do a little bit of that yourself” kind of extension mechanism.

Slightly complicating the issue, the term tends to get reused across a few contexts.

This last type of project is the one that’s of most interest to those in software development who might have to do some incidental web development. Because of how late Safari was to supporting form-associated custom elements, we’re only in the early stages of what Web Component Libraries can offer, but they have the potential to provide ready-made yet powerful and customisable widgets people could use when making websites, services, or web apps, no matter what client-side or server-side framework they’re using.

Described this way, you might wonder how an otherwise innocuous set of web APIs, designed to aid in the making of reusable widgets, so consistently cause tension and conflict among web developers. So much so that it’s almost certainly a very bad idea for me to wade into it with this essay.

Why do Web Components cause online web developer communities to erupt in anger and argument?

To answer that, we need to go back over a decade, at least thirteen years, all the way back to 2011.

WTF happened to Web Components to make everybody so angry? #

The impression you’re likely to get from discussions online would be that web components were created as a reaction to the rampant growth of React-based frameworks bloating websites and breaking accessibility. You couldn’t be faulted for thinking that web component APIs came about as a proposed fix. “Stop using React and use these APIs, they come with the web!”

But web components not only predate the popular misuse of React and the rise of frameworks, they arguably predate React itself.

Web components surfaced to public view in 2011 in a talk at the Fronteers conference by Alex Russell. These weren’t the web components you see today as the code he was presenting was a library, not a proposed standard – that would come later – but the shape of it is extremely familiar to those who have used modern Web Components.

React at this point was still an internal project at Facebook and had only been in use for a few months, if that.

These were two very different approaches towards creating dynamic web-based user interfaces, and they were as good as invented simultaneously.

Their launch as public projects also happened at pretty much the same time with Google’s first attempt to standardise Web Components and Facebook’s first launch of React as an open source project both happening in 2013.

This was, in my personal opinion, the point where multiple schisms in web development began to happen, the break that has hindered web developer discourse since.


I should emphasise here that the following description of events is my opinion only and entirely based on what I saw as an outside observer. I was not privy to any private discussions or chats. I absolutely missed out on some important context, but I can say that this is what it looked like to a lot of people at the time. I appreciate that everybody involved meant well and had good reasons for their behaviour, but people will form their opinion of you based on what they know and see, which doesn’t include the stuff they don’t know about, and especially doesn’t include the stuff you haven’t made public. People do not read between the lines on the internet. At best, they make weird shit up and pretend it’s true.


Web Components at this point were, and I know quite a few people will again disagree with me, an entirely proprietary Google project, much like React was an entirely proprietary Facebook project.

Web components during these first three years, the first iteration that later got called “Web Components v0”, were only ever implemented by Google, only promoted by Google, and its biggest users were Google products and frameworks.

The “polyfills”, itself a term that implies inevitable standardisation, were from the perspective of everybody not using Google’s Chrome the actual implementation of Web Components. They were slow as hell. This made the overall experience of Google properties that used them, such as YouTube, noticeably worse in Firefox.

Not a great look, folks!

To make matters worse – and, again, this is based on my impressions at the time and I know plenty of people disagree with me – a few of the more “enthusiastic” Web Components promoters during these three years were parading around being absolute assholes, to an even greater degree than seemed normal in online developer circles. Many of them seemed to outright believe that a set of APIs made unilaterally by a single vendor would inevitably get railroaded unchanged into a standard – in and of itself a highly problematic idea – and talked about web components being “the future”, implying that every other approach, no matter how new, was “legacy” or somesuch.

Remember, React and Web Components were both released at around the same time – arguably React came later – and here these dudes (almost always dudes) were strutting around calling everything else outdated and obsolete. They claimed to be the only ones properly “using the platform”. Their approach was “standard”.

A “standard” implemented by a single browser, pushed by a single company, with no interest from other browsers visible to an outside observer for at least three years. This is arguably normal behaviour in tech, where a dev-rel’s thankless task of building up good vibes about an API or platform can be tanked thoroughly by a lead dev opening their mouth and managing to simultaneously piss everybody off in 140 characters dumped onto the hive of seething malice that used to be called Twitter.

But, because none of us saw what may or may not have been going on behind the scenes, and because Google was even then at the very least a proto-monopolist… This all? It was not a great look!

It didn’t help that React was in full promise-the-world myth-making mode. It no longer really mattered whether Web Components were superior to React or not. Web developers largely focused on React, which has always had more competent PR than most other open source projects.

Another problem was that nobody else seemed to agree with Google that this specific iteration of Web Components was the future. Firefox and Safari never implemented version “zero” of Web Components. Most developers favoured React’s approach to reactive programming and composition which, arguably, became the conceptual inspiration for pretty much every reactive component framework implemented since.

Everybody who adopted this version of Web Components ended up getting punished for trusting Google.

The painful transition from “v0” to “v1” #

To say that the Safari and Firefox teams were sceptical about web components is oversimplifying things. The template element was relatively uncontroversial compared to the rest. “Autonomous” custom elements – the ability to define brand-new elements with custom names (<my-custom-element>) – were also relatively uncontroversial. The Shadow DOM and “built-in” custom elements – letting web developers directly modify native built-in elements in situ through sub-classing – proved much more divisive.

Many at the time seemed convinced that this reticence was nothing more than a form of road-blocking, that these Mozilla and Apple employees were obeying some sort of secret directive from their bosses to “slow-walk” improvements to the web, but a much simpler explanation is that their concern was genuine, because there was a lot to be concerned about. These particular changes affect the core of how the Document Object Model (DOM) works. The consequences of doing the wrong thing would be both long-lasting and far-reaching, and the web has much too many footguns already. Messing around with the composition and inheritance models of the DOM is something you should legitimately be hesitant to do.

Those arguing for ill intent do have their smoking gun: Apple’s unwillingness to ship the awesome that would be built-in custom elements is, to many, clear evidence that Apple hates the web. That the rationale for doing so involved citing something so clearly bullshit-sounding as the “Liskov Substitution Principle” has frequently been mocked by web devs on social media. (Don’t laugh. Many web devs also think “Model-View-Controller”, “Separation of Concerns”, and “unit testing” sound like bullshit. The web software industry has a substantial curiosity deficit. Apologies for the “X” links, but that’s where nonsense lives, retires, and dies of old age after an active life.)

But, for the purposes of this story, Liskov’s a different kind of gun: Checkov’s gun. And I’m not just saying that because I think “Liskov” sounds a bit like “Checkov” and it amuses me to link the two. This gun will be fired in the third act and not in the way the “WebKit developers hate the web” types think. It turns out the Safari team had a point.

Looking from the outside in, like that creepy dude in the “Sickos” cartoon, the discussions at the time looked involved and intense. The GitHub issue with the infamous “Liskov” remark I linked to above is over 500 comments long, and it’s one of many, not to mention mailing list threads and, I presume, a bunch of calls and in person conversations.

But, eventually, browser vendors settled on a design for Web Components. This “v1” of Web Components was quite incompatible with “v0”, punishing everybody who took Google at their word that their proprietary v0 would eventually become a web standard:

And a bunch more changes that I won’t go into here, but suffice to say migrating from v0 to v1 was a painful process for many.

Moving an existing code base that used the entire Web Component v0 stack to v1 was non-trivial. It took YouTube itself a long while to migrate.

Having to drop v0 and having to redesign Web Components to address the concerns raised by Safari and Firefox did not make the more avid in the Web Components crowd humble or change their tone. If anything, from the perspective of a regular web developer (me, that is), their rhetoric seemed to escalate now they could point to Web Components being an actual standard.

If you are a masochist, you should easily be able to find GitHub comments or Tweets from this period, the early years of Web Components from 2016 to 2020, where a Google employee is calling all other approaches “legacy code” or “legacy approaches” or something similar. If you were a lurker at the time in the discussion threads between them and framework implementers, you’d have noticed that their advocacy also seemed to become much more targeted.

The pressure was on those making the newer web development frameworks to build them using web components, not just be compatible with them, and it looked immense. The targets of this pressure were not Facebook’s React here, but newer frameworks such as Solid and Svelte. These are made by folks that, by and large, are very much in favour of using standards if possible, whereas the React folks seem more inclined to go “meh” and do their own thing.

These newer frameworks are generally much better behaved when it comes to interoperability, exposing native DOM APIs, and performance. Both Solid and Svelte even compile to Web Components meaning you can use them to implement reusable custom elements for your projects. The problem? These pro-web maintainers have differing definitions of what’s “possible” from those more avidly promoting web components.

Some of that comes from the fact that Web Components seem to be largely unsuitable to implementing a functional reactive component framework, which should not be surprising since the two concepts evolved in parallel, not in sequence. It’d have been quite the coincidence if web components had happened to be a useful set of primitives for implementing something like Svelte or Solid.

Quite a few of the people who build and maintain the frameworks themselves hold on to a lingering animosity because many of them feel gaslit about the capabilities of Web Components. Their perception of reality was that these standards were not useful to them at all, but the message they got – insistently – was that web components were the inevitable future of their own frameworks.

This is the background that led Ryan Carniato, the author of SolidJS, to write his blog post Web Components Are Not the Future. That blog post is a reaction to a specific line of rhetoric that has gone unnoticed by most web developers because they were not the ones targeted. That’s why it looked like such an odd, angry, outburst to many and those foolish enough to support their chosen web development tech as if it were a football team took it or the reaction to it as validation that their “team” was better than the others.

What’s missing from this are the reasons why React and its descendants won out over Web Components and their ecosystem. Just because you might not easily be able to use web components to implement something like SolidJS, that’s a very different kind of problem from what most regular web developers are tackling. Ryan might have a justifiable gripe against a small group of over-enthusiastic Web Components promoters, but that doesn’t answer the question that rises when you realise that React and Web Components are generationally peers – to the point of React having its first stable release in 2016 just like Web Components:

Why did React win?

And the answer to that, in my oh so personal opinion, isn’t “because Web Components aren’t great for specific kinds of problems, such as those framework maintainers deal with” no matter how technically reasonable that might sound. Remember, these technical observations and conclusions are popping up after React was well on its way towards resoundingly winning the popularity contest that is modern web dev.

I think it comes down to a simple difference in promises.

Web components promise you that you’ll be able to extend the DOM in ways that are idiomatically compatible in with the rest of the DOM.

React promised us a silver bullet: unidirectional data flow would fix UI development once and for all.

Magical silver bullets will always be more popular than boring old incremental improvement.

React offered two benefits to organisations:

  1. React promised a much simpler model for how to make a UI, one that would magically make coders much more productive.
  2. React also commodified coders by letting them learn primarily to code against a simplified abstraction and the component model itself turned them into organisationally fungible units.

Both of these promises are science-fiction, and experienced React devs as well as React maintainers are both likely to be the first to point that out.

People still believed in these fictions, and many still do.

Both require some explanation.

Unidirectional data flow! It’s magic! #

React’s core innovation was to treat the UI as a virtual data structure that could be reconciled with, or rendered to, whatever stateful group of objects you were using to render the actual User Interface.

This was deliberately agnostic about whether the rendering target was a Document Object Model, a hierarchy of AppKit or UIKit objects, or whatever it is that Android has got going.

This meant you could structure your UI components as a set of functions or classes that together took the app’s state and returned a tree-like data structure and, because these were in theory just accepting and returning data, you could steal ideas from functional programming – immutable objects, no side effects, baby! – to make the whole process easier. Instead of complex manipulations full of potential side effects, your mental model of your GUI app would be something along the lines of:

UI = f(state)

Events and actions generated by the UI modify state. That triggers the f(state) part again. The data structure returned from the components is diffed with the existing UI and the UI is updated based on that diff to reflect the new state.

This is what the React scene calls “unidirectional data flow” (“one-way data flow” in more recent documentation) because data only flows from state to UI, whereas the data that flows from the UI back into state is labelled “actions” or “events”, which means they aren’t data, silly. No, the data the user is typing into the form isn’t data. It’s an action. Here, read this rambling blog post and library of old-style Twitter threads that explain it. Of course, you need to be smart enough to understand that data isn’t data except when it’s data, and it’s always going in one direction except when it’s going in the other, in which case it’s a cycle, which makes it all one direction.

Something about drawing it as a circle somehow makes people miss the fact that stuff is obviously flowing in multiple directions. Not just up and down, but sideways within a component and often React apps just completely drop the pretence and shove the data directly to where it belongs using things like the Context API.

But, sure, the data only flows one way. You just need to have curious definitions of the words “flow”, “one”, “way”, and “data”.

That, however, isn’t the problem with the idea. It’s actually a neat idea that forces you to rethink some of the basic principles of GUI software development. The problem is that it’s pretty much only an idea: a myth. What we get in practice is almost always quite different in very important ways.

The issues, in no particular order:

But, Baldur, you don’t understand! You don’t get React! Oh, I understand. When I use a model object that’s connected to a view object using a coordinating object (controller, presenter, or whatever), then sure that’s MVC or MVP. But, when you do it, it’s a parent component that’s using the context API and hooks to coordinate a state object with a functional child component.

Effectively, as soon as you try to do meaningful work with React, the “unidirectional data flow” ceases to be an architecture and becomes an origin myth. Meanwhile the actual architecture is whatever uncoordinated “I can’t believe it’s not MVC” hocus pocus is popular in the React scene the week the project was started.

For a very brief period, React’s tag line was something like “The V in MVC”, but they quickly gave that up because there’s so much more in React than just the components. They have an entire array of state and event management tools that together assemble whatever app architecture you want.

React has always been the whole kit and caboodle, not just a “V”. It’s just never enforced a single, coherent architecture and instead provided hooks and tools with no direction and soft boundaries.

But it promised magic, and people didn’t seem to notice that, in time, making and maintaining a React app was just as much work, if not more, as those made using more traditional approaches. Moreover, React apps that were explicitly structured using the Model-View-Controller architecture from the ground up tended to, in my personal opinion as an extremely biased outside observer, be less problematic over time than those that went all in on the unidirectional gravy train.

A promise of magic is always going to be more popular than promising to give you sensible – the “wear practical shoes, it’s going to be a long walk” kind of sensible – steps forward. Even if Web Components had met with universal approval among browser vendors right out of the gate, it’s unlikely they would have ever won out over the promise of making the development of dynamic user interfaces magically easier.

It should not be surprising that more modern component frameworks, ones that aren’t saddled with a largely fictional core concept, such as Vue, Svelte, or SolidJS are by almost every metric faster, lighter, and more productive than React. Preact even manages to be a far superior “React” than React itself.

React has become a bloated carcass of false promises, misleading claims, and unending layers of backwards compatibility – the wrong kind of backwards compatibility, as they still occasionally break your fucking code when updating.

Pretty much anything else is a better tool for pretty much any web development task. There’s also more genuinely useful innovation going on everywhere except React. SolidJS came up with signals, a genuinely interesting method for handling state. Preact adopted it and arguably came up with a more interoperable implementation of the idea. Vue and Svelte both hew closer to web norms in interesting ways. If you want innovation and experimentation, there’s a lot of it going around. Just not in React.

Promises of magic only go so far. Since the consensus of the overall effect that React has had on the web is currently ranging from “on the whole, probably not great” to “the only thing that would be worse for the web would be a comet wiping out all civilisation, and even that would be a blessed relief,” if that were all, React would not be nearly as dominant in the web software industry as it still is today.

That’s where commodifying developers comes in.

WTF does ‘commodifying developers’ even mean? #

In basic terms, what management wants is the ability to add, remove, or move around staff while pretending that said staff is largely interchangeable and that the moving around doesn’t mess things up in and of itself.

This is the case for most modern companies, not just the ones in tech.

Any form of expertise or specialisation makes that idea even more fictional than it already is. Employers generally love whatever scheme they find that can narrow the skill sets required to do a task down to something that can be clearly defined in market terms. One way to do that is by requiring specific degrees and certifications. Another is to standardise on using specific tools and platforms and hiring for those. You don’t try to hire people with good problem-solving skills, an ability for writing clearly, an affinity for numbers, or an understanding of how computers work that helps them pick up new apps quickly. You hire for skills in “Word” and “Excel”. That lets you hire quickly, fire quickly, and move people around as if they were, I don’t know, Legos or something. (Sorry, “Lego bricks”, he corrects himself before the pedants light their torches and grab their pitchforks.)

This is hard to accomplish in software development. The entire job is basically discovering and solving poorly-defined problems with squishy human elements at its centre. React stepped in just in time to help managers en masse fake their way to a promotion during a low-interest-rate bubble.

Much like “Word” and “Excel” have become the “fungibility” stand-ins for “can write clearly” and “understands numbers”, React has become a stand-in for “UI software development”.

It hides the complexities of what’s happening underneath a layer of abstractions. Instead of learning a host of DOM querying and manipulation APIs or the workings of platforms like UIKit, you just return JSX. Instead of exposing the various nuances and intricacies of event handling on each platform, you get one form of event delegation and you’re going to like it. The JSX data structures are so abstracted away from the actual platform that they even offer approaches for you to pretend there isn’t any difference between a web, iOS, and Android app and let you target (or try to target) all with a single code base. React developers interact with a drastically simplified view of the world.

Because that simplified view has limitations, the underlying platforms push their way into the job with alarming regularity. But because the point of hiring for React is to not hire people with the specialised skills to deal with the nuances of underlying platform, most of the time everybody reaches for a dependency instead. Something made by somebody with a deeper understanding of the underlying platform that packages up a set of solutions that those with full-on React blinkers can use.

This heavy reliance on dependencies compounds React’s many pre-existing performance problems. The bloat of these web apps isn’t accidental but a by-product of how React is used by management.

(I’ve written about this before, for those who are curious.)

Understanding how the DOM works is a hard requirement for using tools to extend the DOM. This made Web Components a non-starter. Web Components didn’t help enable the massive growth in head count that drove much of the decision-making in tech from 2011-2022.

Bear this in mind below, when I go into some of the technical limitations of Web Components. Those limitations are not the reasons why React won. React ended up the winner because it devalued software developers and made promises it could never fulfil.

The schism this has created #

If that were all, then web components would just be a textbook case of over-aggressive promotion and hubris leading to self-sabotage, and a useful but limited technology losing out primarily because of the market’s many dysfunctions. Both are a regular occurrence in tech and software development.

But has had other consequences. It has fractured the online web development community.

The React-versus-everybody-else schism is obvious, but the other schisms are arguably more tragic.

Specifically, drama surrounding Web Components and a steady parade of similar events has fractured the part of the community that is trying to get the industry to stop using what has now become the actual legacy framework: React.

Instead of a united front that promotes a set of approaches as alternatives to React, each with differing optimal use cases, we now have three factions that do not collaborate nearly as much as they should:

  1. “Use the platform”. Standards-oriented web developers who have been promoting the idea that for many – if not most – web development use cases, it’s faster, cheaper, and more reliable to just use browser APIs directly instead of through the indirection of a framework. Many people in this group tend to be against all frameworks, no matter how they are designed or implemented. A minority is even outright against the notion of JavaScript in the first place.
  2. “Web Component” framework implementers that, for one reason or another, associate themselves with Web Components as a stack and some with the “use the platform” movement. These frameworks have usually been designed from the ground up to accommodate web components and inherit their limitations, at least on the client side.
  3. Other reactive component frameworks that, for equally arbitrary reasons, don’t associate themselves with web components or the “use the platform” movement. These are the ones that have felt targeted by web component advocacy. Most of these frameworks are already highly interoperable with web components and never had React’s issues with using third-party web components, so Web Components advocacy that targets them seems to be entirely about how the frameworks themselves are implemented. Because of the adversarial relationship between them and the other two groups, these communities tend to side with what is arguably a more genuine threat to their existence: React.

The differences between these groups are not technical. WebC and Svelte both use single file HTML components and compile to HTML, CSS, and JS, usually mediated through a chosen site generator – “meta-framework” if you will – Eleventy and SvelteKit respectively, and they both include custom elements as compile targets. Any debate that presupposes a fundamental technical difference, beyond just their relative sizes, between these two frameworks is already in the wilderness.

Using Lit is not morally superior to using Vue. Different frameworks have different trade-offs and, yeah, that does mean that sometimes one is a more productive choice for a situation than the other. Sometimes that means that Lit, Eleventy with WebC, or Enhance, are the better choices. Sometimes it’s Vue, SolidJS, or Svelte.

The core difference between the second and third groups is that the third group, like SolidJS for example, often tried at some point to integrate various parts of the Web Components stack into the implementation of their projects and became very frustrated with the technology and now many of them seem to be quite bitter about web components as a result. They were promised platform fundamentals that – because they were the future of frameworks – would work as building blocks of their own projects.

This is a needless divide. “Use the platform” developers, web component frameworks, and modern reactive frameworks can all use and benefit from web components today. You can use a web component in Svelte and you can use Svelte to make a web component. WebC will work well using a Svelte component that has been compiled to a custom element. Those who eschew frameworks can use components made in either in their projects.

Web Components, if the goal was to improve interoperability and reuse, are a smashing success. They are an incredibly useful building block for modern websites that’s only going to become more useful now that we have broader support for form-participating custom elements.

Lea Verou does a good job of explaining what Web Components are good for in her own post on this topic:

Frameworks already use native HTML elements in their components. Web components extend what native elements can do, and thus make crafting project-specific components easier across all frameworks (as well as no frameworks).

I tried to find a better way to phrase this, but Lea already came up with the perfect framing. Most devs could just stop here, read Lea’s post, nod a few times as they scroll, and be done with it. (Though, you’re most of the way through this behemoth of an essay by now, so you might as well read i to the end.) This is how Web Components work. This is how we should be using web components and frameworks.

(And, by the way, the fact that the Font Awesome people have hired her and Zach Leatherman have turned my scepticism about Web Awesome into outright enthusiasm, even if Zach isn’t involved in it directly. I’m very much looking forward to seeing what they and Font Awesome do in the future.)

However, the underlying technical reasons why web components are a bad fit for some software architectures are still there, largely unexplored.

I’ve had to use web components quite a bit, myself. I largely like them. I even see the appeal of the Shadow DOM, despite its many issues. I’ve used web components (no framework), Lit (a framework designed with web components in mind), and Svelte (a reactive component framework) for complex projects, and I’ve experimented with a lot of the rest with prototypes. But, in my experience, using web components, with or without Lit, to implement a big project end-to-end was usually harder and more complicated than using a reactive framework for the same purposes.

It was also sometimes harder than using no framework at all. When people say “use the platform” they aren’t just talking about the handful of Web Component APIs. The platform has more to offer than Custom Elements and Shadow DOMs.

Web components can be suboptimal for many projects for reasons that are very similar to the reasons why they aren’t useful for the internal implementation of frameworks like Solid or Svelte, but I can’t speak for framework implementers, only for myself.

Web Components are defined in terms of alterations to the DOM #

The first issue has to do with how web components interact with the rest of the Document Object Model (DOM).

Every API that’s commonly bundled under the “Web Components” umbrella is defined in terms of their alterations to the DOM: Shadow DOM, custom elements, even the <template> element to some degree. Using them requires injecting them into the DOM tree at specific points. That quickly becomes problematic because the semantics of the DOM are highly dependent on specific hierarchies.

The list could go on forever because this is a fundamental part of how both HTML and SVG are designed, so it’s happening throughout both. The DOM fundamentally uses hierarchy for composition. The same element will have different functionality and meaning depending on what parents it has.

To use object-oriented terms, the DOM relies heavily on both composition (“X is extended by having a Y”) and inheritance (“X is extended by being a Y and inheriting its features”) in a complex system that does not easily allow for intermediaries. A list only works if the parent ordered or unordered list element is in a direct “has a” relationship with child list elements. Direct parent-child element relationships is one of the building blocks of DOM composition.

Components, as an architectural concept, need to add behaviour to and render into these kinds of hierarchies without breaking their structure.

Components in many reactive frameworks solve this by not injecting themselves into the DOM at all. They return elements that the rendering context will integrate into the DOM, but they generally won’t be a part of the explicit DOM hierarchy themselves. You don’t want a ListComponent to wrap the li elements it returns in <list-component> tags because that would break the semantics of the DOM. The list would no longer function as a proper list. Components in these frameworks generally exist as an abstraction outside the DOM. A rendering context takes that abstraction (often a “virtual” DOM) and render to the “actual” DOM.

That many developers misuse frameworks doesn’t change the fact that modern frameworks are specifically structured around the ability to deliver semantic and accessible DOMs without breaking the parent-child composition that’s integral to modern HTML.

This is React’s positive legacy to the world of web development: a way of making components that don’t interfere with the Document Object Model’s core methods for composition and extension. That positive legacy has been carried forward and improved by modern web frameworks.

Once you lose the mythical “unidirectional data flow” baggage, and aren’t trying to build the universal UI engine, “return a data structure that gets diffed and merged with a corresponding part of the DOM” is actually a neat and productive idea that fits extremely well with traditional approaches to working with the DOM, such as template rendering.

If you look towards approaches to state management that are more akin to the matching up of models with views through some coordination, functional reactive components actually begin to look kind of awesome. They aren’t quite the magic solution React promised, but they are a meaningful and substantial improvement.

That’s where we’re at with modern frameworks. That’s what many of the Web Components frameworks are trying for as well, even though they are held back by how custom elements inevitably inject themselves into the DOM, because custom elements are defined in terms of, well, custom DOM elements.

That “injection” can make the DOM dysfunctional by interfering with the hierarchy many elements require. It took years for them to finally fix some of the issues Shadow DOM has with forms, and we still occasionally have issues with it interfering with aria- property references, which are used to properly expose the UI to accessibility tools, and labelling across components. How selection APIs interact with Shadow DOMs are still a mess (though hopefully not for long). Custom elements can’t easily be used to render list or table child elements directly. They don’t work at all for extending SVG, which is the only vector drawing surface available to the broader web.

Even if you pretend Safari doesn’t exist and just look at Firefox’s progress, their support for form-associated elements was released in 2021, five years after the “v1” release of web components. It’s been a long road.

These standards seem legitimately hard to implement, which is what you’d expect once you realise they’re messing with some of the core structures of the web platform.

This issue with hierarchical interjection is why you occasionally hear framework types say that web components should mostly be used as “leaf nodes”. If the web component has no children visible to the framework – is a “leaf” not a branch – then it can’t break the hierarchy. It’s a useful rule of thumb if you’re working in the framework world and fits well with how Lea Verou defined the relationship between web components and frameworks in the post I quoted above.

The CSS property display: contents; also goes some way to alleviate this issue as it can be used to effectively “disappear” the custom element from the rendering tree, so it doesn’t interfere with, for example, the parent-child relationship between a <ul> and <li> element so it only partially interferes with existing parent-child relationships. (Turns out, even though I’ve spent this entire essay pointing out how complicated and involved the DOM’s composition through hierarchy is, I’d forgotten how involved it was in this paragraph. display: contents; is supposed to preserve the semantics of the element in tree while removing it from rendering. This is useful, but it doesn’t really fix how web components interact with DOM composition, which goes to show just how tricky the problem is.)

This doesn’t address the SVG gap, either. Due to their nature as, well… elements, custom elements will always have to play by the rules of DOM composition.

Still, overall Web Components are in much better shape than many expected they’d ever be in, even if they aren’t everything that was promised.

We’re still missing the ability to create custom elements that extend built-in elements. That would be the last remaining issue to solve and would let you deliver web components that render elements like <li> or <td> without interfering with the hierarchy. It is the key to perfecting web components and bringing about the future we were promised.

Or is it?

There’s reason to believe that the Safari team might be have been right to refuse to implement the is attribute and not let custom elements extend built-ins.

Inheritance is fucking tricky #

When you work with markup, hierarchy can serve as a very powerful form of composition. But, as I outlined earlier, that hasn’t worked too well for custom elements until fairly recently. In practice, their inclusion in the DOM hierarchy often interferes with the composition of built-in elements with little benefit to the custom element itself.

Beyond that, there’s a tension inherent in how custom elements were designed. They are simultaneously late- and early bound.

They are dynamically late-bound to an element name, <my-custom-element> for example, that then automatically works in the markup and DOM. A script can define a custom element with any valid name it wants. A component or function that renders HTML doesn’t need to import that script directly or have any knowledge of the custom element. The developer can change the composition of a DOM tree simply by mapping a different custom element implementation to that name. The communication between the custom element and its surrounding context is done through messages – events – attributes, and object properties. None of the elements in the surrounding context need to even be aware that the implementation of the custom element has been switched to a different one.

This is a form of late binding, or loose coupling, and late-binding is extremely useful for building applications with user interfaces because it lends itself to building complex functionality through composition without, well, binding you to a rigid structure that can be hard to alter as the design realities of the interface are surfaced. You can still easily modify the project as the app gets tested and user feedback pours in.

As Alan Kay explained:

Late binding allows ideas learned late in project development to be reformulated into the project with exponentially less effort than traditional early binding system.

This is a good thing.

At the same time, the implementation of custom elements is based on highly specific inheritance that can’t be easily remapped. If you’re rendering in a browser, the HTMLElement class is always going to be HTMLElement. Even though you can technically replace it – JavaScript is a late-bound language as designed – doing so risks breaking the DOM entirely.

That means that the implementation and state management of a web component is tightly coupled to its ancestor classes and that will generally make reformulations and alterations harder. Moreover, the DOM’s existing class inheritance tree was not made with web developer subclassing in mind. Its inheritance is complicated and a mess. A few elements actually share interfaces, such as the <q> and <blockquote> elements sharing the HTMLQuoteElement interface and a whole mess of legacy elements sharing the HTMLUnknownElement interface. This complicates matters. Extending built-in elements also has an extremely controversial history and frameworks messing around with built-in language objects, such as Array, have directly hindered platform development and standardisation multiple times in the past.

But subclassing those should be fine, right? The subclasses should break anything, right?

Well, this is where Liskov’s Gun goes “blam!” because the answer to that is “probably not”.

Let’s look at the Liskov substitution principle itself, the one cited by that WebKit developer all those years ago:

Subtype Requirement: Let ⁠ ϕ ( x ) ⁠ be a property provable about objects ⁠ x ⁠ of type T. Then ⁠ ϕ ( y ) ⁠ should be true for objects ⁠ y ⁠ of type S where S is a subtype of T.

Well that clears that right up! So clear. So obvious. That doesn’t sound like gobbledygook to regular humans at all!

Effectively, paraphrasing the above-linked Wikipedia page into human(ish) terms, what it means is that if S is a subclass of T objects of type T may be replaced with objects of type S without altering the correctness of the program.

That is, if you subclass HTMLInputElement, instances of your subclass need to work correctly whenever they’re substituted for an instance of the original HTMLInputElement and, because JavaScript as a language isn’t really able to offer any such guarantee, this principle can’t easily be enforced in the browser runtime.

So, you’re left with debating the principle itself, which is an argument that can definitely be made on an abstract level, but once you narrow it down to DOM elements specifically, I think the overall principle becomes quite compelling.

Remember the section above about how the DOM uses hierarchy (“has a” relationships) for composition? How elements such as <table>, <li>, and <input> to name a few, need specific parent-child relationships to function properly?

Well, the built-in subclasses that provide the interfaces these elements use are an important part of making that composition work.

One of the more important uses cases for web components is to let people implement and distribute reusable widget that are interoperable across multiple projects, frameworks, or even organisations. Much of the standardisation work surrounding web components has been towards ensuring that these packaged widgets don’t interfere with the outside DOM and that they won’t be interfered with by it in return. That’s what the Shadow DOM is for.

Because of this, you can take a <custom-date-picker> widget and include it in your project without much worry, provided you aren’t using it to wrap elements that require a direct parent-child relationship with specific other elements to work. Now that we have broad support for form-associated elements, this mostly works and provides a good foundation for interesting projects such as a new generation of open source widget libraries.

But if you take an <input is="custom-date-picker"> element, you don’t have the same assurances. You don’t know whether you can actually use it in all the same ways you can use a regular input element. JavaScript can’t offer you that guarantee. Remember that every built-in custom element begins life as their original parent class before they get upgraded by the custom element script, so it could start off being compatible but then suddenly become incompatible when a script loads.

Seen from this perspective, broad adoption of custom elements that extend built-in elements does seem likely to cause serious and hard-to-solve bugs in a non-trivial proportion of the projects who would use them.

Or, in the words of Ryosuke Niwa, the WebKit dev who originally cited the Liskov substitution principle, writing on the topic eight years ago:

One fundamental problem is that subclassing a subclass of HTMLInputElement or HTMLImageElement often leads to a violation of the Liskov substitution principle over time. Because they’re builtin elements, they’re very likely to be extended in the future. Imagine that we had this feature supported before type=date was added. Then your subclass of HTMLInputElement must also support type=date in order to satisfy the substitution principle. Similarly, imagine we had added this feature before srcset was added to HTMLImageElement and you wrote a subclass of HTMLImageElement. Then not supporting srcset results in the violation of the principle again.

In addition, none of the builtin elements are designed to be subclassed, and in fact, we don’t have builtin subclasses of HTMLElement that are also builtins. This limits the usefulness of any sort of subclassing. Furthermore, we don’t have any hooks for the processing models and internal states of builtin elements such as the dirtiness of HTMLImageElement’s value and when and how HTMLInputElement picks the appropriate image.

I mean, yeah. When it’s put this way, it does sound problematic.

Overall, class inheritance versus composition is a classic question of trade-offs in software development and in software with graphical user interfaces – especially web-based ones – I’d argue that inheritance is the wrong trade-off for this task in particular because it’s interfering with an already complex inheritance system, which in turn can complicate even further how custom elements interact with the hierarchical composition of markup.

This is where the difficulty of using Web Components to implement Functional Reactive Components enters the picture #

Reactive components, as implemented by most modern frameworks, make a different trade-off. They tend to be functional, even if they aren’t implemented as functions. They still inherit React’s issue of only working when rendered to a DOM with all the side effects that entails, but because most of them are thinner abstractions than React and target fewer platforms, I’d argue that the issue has become more manageable. You render to a DOM element and test that element as a data structure. With React you often don’t even know if the final target is a DOM element, HTML string, Android view, or UIKit view.

Instead of the subclasses you have to deal with in Web Components, you often have a series of function invocations that treat the returned DOM representation as, ideally, side-effect-free data they can modify and pass on. Immutable data structures are popular in among this crowd for good reasons. The representation that’s returned usually isn’t an actual DOM fragment, but gets resolved and rendered into the “real” DOM by a rendering context.

To quote Mayank

I can create an <Input> primitive component that wraps an <input> element. And I can create a <ThemedInput> component that wraps the primitive <Input>. Then someone else can take my <ThemedInput> and create a <PasswordInput> on top of it. After all of these components are resolved and initialized, the final DOM tree should really only contain a single <input> element.

The intermediate components are not (always) relevant for the DOM tree. The browser doesn’t care how the code on my website is organized. In other words, components are totally made up. That doesn’t mean components are not useful; just that they exist in a different plane. This is what makes it possible to compose all the way up to a <Page> component

Trying to accomplish this with web components is tricky and the end result is usually a deeply nested tree of custom elements. You can hide some of that in Shadow DOMs but that introduces a new set of problems caused by the Shadow DOM itself.

Using web components as native-like components that are entirely external to the rendering process bypasses this in part, but if you are authoring the “leaf” web components yourself, I’d argue that the emphasis on inheritance and de-emphasis of composition will make the creation of complex widgets harder and more error-prone than it would be with a framework’s component system.

The interoperability benefits and the fact you’re building with web standards, which generally do not change once they’ve been shipped across multiple browsers, gives you stability and should lower development costs in the long run. For many, if not most, organisations this trade-off should be worthwhile. But there’s a good chance that using web components to implement the core architecture of your app could make the task harder. You’re likely to have to lean heavily on inheritance and classes and eschew composition for implementing the logic and flow of the application.

It absolutely can be done, either by minimising your use of class inheritance or by being thoughtful about it, but one of the reasons why reactive component frameworks, even though all the modern ones are considerably younger than web components as a technology, are so popular among devs, even the developers that don’t have to use React for organisational or political reasons, is that their architecture makes these tasks a little bit simpler.

Interlocking functional components that use signals for state, for example, are easier to use and reason about than interlocking class-based web components that use properties and events for state and reactivity. This does not matter that much if the web component is a “leaf” node, the last stop on the line so to speak, but it begins to matter a lot if these components are being use throughout an app’s overall structure.

Thankfully, you don’t have to use a framework to get these features. You can write functional components that use signals for reactive state using either standalone lit-html or uhtml tagged template libraries. You don’t need the full framework baggage. Some frameworks, such as Enhance offer similar capabilities that are functional and reactive out of the box. We have a spectrum of options that range from using only platform APIs, to specialised libraries, to lightweight frameworks, and all the way to fully-fledged “batteries included” web development frameworks.

One size very much does not fit all. We should feel free to choose the toolset that’s appropriate for our task and our project. We should not saddle ourselves with unnecessary work out of habit or misplaced loyalties towards one “team” or another.

And none of us should be using React.

Web components don’t really work for quite a few of the tasks we were initially told they would be good for.

Pointing that out isn’t a criticism of the web as a platform. Nor does it imply that you should always use a framework or that you can’t get anything done using just platform APIs with a small selection of libraries.

All it means is that this particular slice of the web platform turned out to have limitations we need to be mindful of when we do our work.

Given the limitations I went into above, it doesn’t surprise me that many framework implementers found it hard or impossible to use web components to implement a framework for reactive functional components. There’s a clear mismatch there.

It also casts some light on what framework maintainers mean when they say that Web Components are a bad direction for web development. If web components, specifically, represent the future of web development, then it’s a future filled with awkward class inheritance hierarchies, even deeper markup hierarchies, and layers of code whose only purpose it is to mitigate how custom elements and the Shadow DOM interfere with regular HTML and SVG composition. It’s a vision of a future where web development is substantially harder, less powerful, and less productive.

Fortunately, I don’t think that’s a future anybody wants or is even working towards. Web Components are great for what they are, genuinely great, but they don’t represent the extent or promise of the platform. There’s so much more happening. Recent additions to the web platform are squarely aimed at a much brighter future, one that helps developers implement great experiences without sacrificing usability, performance, or accessibility.

That’s just to name a few and does not even begin to go into the many potential future standards that might improve things even further. Web standardisation today is in a much better place than it was a few years ago and instead of pushing the web harder in the direction of web components, most people seem content to sand off their rough edges and make them work better with the rest of the platform, using features such as Declarative Shadow DOM and ElementInternals. We’re letting web components “be their best selves”.

Over time, these improvements to the platform might together change how we do web development in meaningful ways:

  1. Fewer and fewer web projects will outright need frameworks to be manageable and many of those that do will be able to make do with much simpler ones that are mostly for enforcing sensible architectures (MVC, MVVM, ECS, etc.) and curating a useful standard library of tools.
  2. Interoperability between framework and non-framework code, as well as across frameworks, should improve.
  3. Many frameworks might get lighter, especially if the Navigation API becomes viable, as they drop big chunks of code in favour of using platform APIs.
  4. Component or widget libraries that are built using web components might become much more broadly usable, both with frameworks and not, without imposing the costs in terms of payload, performance, and accessibility many of them have today.

All of this matters because we really do need web apps #

It’s increasingly popular to rail against the existence of web apps. People get angry about how much crap gets loaded into your average website. The web is filled with ads and tracking software that are making it less and less usable. Quite a few sites are also implemented using bloated frameworks that would be barely acceptable if they were being used to implement a revolutionary killer app but are completely beyond the pale for building a regular website or dashboard.

The web is a mess that seems to only be getting less usable and messier by the minute.

You’d be justified in dismissing the entire concept of web apps: Websites don’t need this. This is all bullshit.

But none of the tech I’ve discussed is really to blame for what has happened to the web. That lies squarely with advertising as a business model and React’s unique dysfunctions. Web Components, new web APIs, and newer more modern frameworks of every kind represent our path out of the web crisis and shouldn’t be rejected just because they involve JavaScript.

This isn’t just a question of fixing web’s current issues. We need web apps. We both need more web apps, and we need more capable web apps.

Mainstream Operating Systems – Android, ChromeOS, Windows, macOS, iOS, and the rest – are increasingly closed platforms whose primary software markets are managed through conservative and puritanical central committees: their hidebound App Stores. The platforms themselves are constantly shifting and changing depending on what fad the OS vendor is chasing to boost its stock price. For example, many iOS app developers sacrifice months of every year updating their apps to keep up with platform changes. If they don’t, they lose customers because their apps are broken or perceived to be broken. These are months they could be using to implement genuine improvements to their apps and to build their businesses.

Those businesses can only use the specific business models approved by a platform’s central committee, are only allowed to include the content deemed morally appropriate by that central committee, and need to have every update and minute elements of its marketing strategy approved by faceless serfs working in a soul-draining bureaucracy.

More importantly, these institutions also do not stand up to authoritarian powers. They will bow down to the demands of China, and they will not stand in the way of an authoritarian takeover of a democratic country.

We need web apps.

Web apps are the only open and standardised GUI software development and distribution platform that’s available to us. Discarding them out of spite or annoyance is exactly the sort of situation that the phrase “don’t throw the baby out with the bathwater” was coined for.

We need this. And, thankfully, web standards are slowly but surely stepping up. Now we only need to make sure that OS vendors, such as Google or Apple, don’t stand in its way.

And we need to make sure we don’t stand in its way.