all posts

I’m pretty open about my many issues with “Web Components”. I think the entire idea is a bit misguided. However, I do understand that the web doesn’t do breaking changes and I’ve long been diving into the various APIs that exist to create so-called “web components”. I’ve previously written about what web components can do where I looked at the capabilities of custom elements, shadow DOM and template elements.

I’ve even built an experimental, server-first UI framework called Solenoid which uses custom elements and signals to create a “resumable” UI framework. That is its own interesting rabbit hole and you can watch the talk I gave at React Summit introducing it to learn more.

In this post, I want to share what I learned while trying to build various things with the APIs from web components, and where it falls short.

#Custom Elements need an event for the closing tag being mounted

Custom elements are, in my opinion, the most useful API under the web components umbrella, but what they provide is fairly simple: mount and unmount events. This is vastly superior to using MutationObserver which is the only other alternative. Solenoid makes extensive use of custom elements to make streaming interactivity possible. However, while building Solenoid, I discovered the most annoying detail in how the onConnectedCallback event of custom elements work. When streaming some HTML with custom elements, onConnectedCallback fires as soon as the opening tag is parsed by the browser. This is too early as it becomes impossible to run any logic that depends on knowing the children of the element.

I understand why this is the case. The HTML parser is the browser is forgiving by design and will create a valid DOM even without a closing tag. But that ends up punishing correct HTML to deal with the edge-cases of malformed HTML.

#Proposal: onCompletelyConnectedCallback

Add another method to the custom element API that waits for the element to finish rendering before firing. In order to make this work, the browser should wait for one of the following to happen:

#Shadow DOM is all or nothing

Shadow DOM does a lot, and its encapsulation features are more of a hindrance than a help when building apps. CSS features such as @scope provide style encapsulation in a way that developers actually want, and the way it divides up the DOM into light and shadow trees just adds unnecessary complexity. However, if it is going to exist, it should at least have some knobs available to opt-in or out of different kinds of encapsulation.

#Proposal: An opt-out for style encapsulation

Add a new property for shadow DOM (and for the <template> element used for declarative shadow DOM) that allows the developer to opt-out of style encapsulation. In a world where atomic CSS is extremely popular, I don’t want to deal with injecting CSS files in every single shadow DOM just to get the styles I want.

On the flip side, @scope already gives us a way to encapsulate CSS and nothing else.

#Slots only work on the top level

This is by far the most disappointing limitation I discovered while learning about the web component APIs. Slots are a powerful feature and can enable out of order streaming and “portals” without any JavaScript. However, their use cases are greatly limited by the fact that slotted content must be a top level child of the element with a shadow DOM.

#Proposal: Slots that work on any level

Support the slot attribute on deeply nested elements and slot them to the nearest ancestor element with a shadow DOM.

#Proposal: Support slots without shadow DOM

Additionally, it’s cumbersome to opt-in to shadow DOM just to use slots. Add an API where a container element can be marked as a slot container. and then any children with a slot attribute should be slotted to the nearest <slot> element with the same name.

Thankfully, something similar is already being proposed, but I think extending Slots for this use-case makes for a better and more consistent API.