Web Components 2023 Spring Update

EisenbergEffect
12 min readMay 1, 2023

A couple weeks ago, members of the W3C Web Components Community Group, including browser vendors, library authors, and community members, met face-to-face to discuss new Web Component standards and platform improvements. A wide variety of topics were discussed, with consensus reached on a number of issues. In this post, I walk through the topics and their outcomes.

IMPORTANT: Before diving into this article, you may want to make sure you have the requisite background knowledge. If you haven’t read it already, please read my post titled “2023 State of Web Components”.

Day 1

Declarative Shadow DOM

The first day started out with discussion on adding more declarative capabilities to the Declarative Shadow DOM (DSD) feature. In particular, while there is a way to declare Shadow DOM, there isn’t a way to declare all capabilities associated with other Web Component features. Examples of other capabilities you might want to declare include:

In general, the group resolved that all new Web Component and Shadow DOM related features need to be designed with DSD in mind from the beginning (I don’t think there was ever any disagreement on this; it just needed to be explicitly stated). The group also resolved to explore a few general approaches that could be used for multiple features, with the goal of finding the best approach to pattern future updates from.

Additionally, the group reached consensus on the approach to custom slotting in DSD as proposed in this issue. Here’s what a declarative shadow DOM would look like, showing custom slotting declared according to the proposal:

<custom-element>
<template shadowroot="closed" slotassignment="manual">
<slot initialelements="0"></slot>, <slot initialnodes="5 7"></slot>
</template>
<custom-child>hello</custom-chid>
<custom-child>world</custom-chid>
<custom-child>shadow</custom-chid>
<custom-child>DOM</custom-chid>
</custom-element>

When the slotassignment is set to manual then you can use initialelements to specify the index of child elements to be assigned to that slot or initialnodes to specify the index of nodes to be assigned to that slot (so that text can be accounted for).

NOTE: While the API above can be written by hand, it will most often be used by server-side and static site generation frameworks.

Selection API

If you are building rich text editors as Web Components, one of the challenges you may run into is handling content selection across Shadow Root boundaries. A new API called getComposedRange() has been proposed to solve this issue. Chromium is supportive of the proposal and Mozilla has already started to implement. Mozilla’s work implementing has revealed a number of open questions that the group needs to resolve:

  • How should the proposal handle DOM mutations that happen within the selected range after it is created?
  • Can the group better spec out the various types of ranges? The terms internal range, static range, and composed range have been used but are not all well-defined. Clarifying this would result in more consistent implementations across vendors.
  • Should multiple ranges be supported at once? WebKit is interested while Mozilla already supports the capability. Chromium wants to make sure that it is done in a way that doesn’t break existing sites.
  • What is the best way to serialize a range that includes Shadow DOM?

In general, the group was supportive of the effort and plans to proceed once the above questions are adequately answered.

The discussion also revealed a potential missing API at the platform-level. Currently, there is no way to turn a composed DOM tree (tree of trees) into a flattened DOM tree (single tree). This is particularly useful in copy/paste scenarios. So, a separate discussion around this resulted in a proposal to extend TreeWalker to support this use case.

DOM Parts API

The discussion on DOM Parts started with the need to find a way for the API to support Declarative Shadow DOM (DSD). A few of the important requirements are:

  • There needs to be some sort of DOM Parts syntax that the server can emit, which won’t break browsers that don’t support it yet.
  • The syntax needs to work both in template elements and in the main document.
  • The syntax needs to support nested parts.
  • There needs to be a high-performance API on the client to acquire all emitted parts within a tree, in support of fast hydration/resume.

A representative from Google proposed a new syntax based on processing instructions, which would mark out parts within a DSD.

<html>
<section>
Name:
<?child-node-part?><?/child-node-part?>
Email:
<?node-part?>
<a><?child-node-part?><?/child-node-part?></a>
</section>
</html>

The proposed instructions were:

  • <?node-part?> — This would create a DOM Part associated with the next sibling node. This is important when you want to target an attribute on a node, rather than its content. Notice how the node-part proceeds the <a> tag in the above example, since we want to target the href attribute.
  • <?child-node-part?> and <?/child-node-part?> — These would be used to wrap content as a part. Notice how they mark the location of where the name and email text would be rendered.

This proposal launched a whole range of spirited discussion. Key concerns that were raised included:

  • This syntax is difficult to read and write, compared to the more common {{}} syntax used in templating libraries.
  • There seems to now be a need for two different syntaxes: one for marking parts in the DSD scenario (processing instructions) and another for marking parts for client-side bindings (curly braces).
  • There seems to be different intentions with this new syntax from the original DOM Parts proposal, which was primarily focused on high-performance template instantiation.

There was a lot of back-and-forth discussion and unfortunately no consensus was reached. What was resolved was that follow-up meetings needed to be scheduled so that this could be discussed in much more detail, with additional options being explored. The first of those meeting is happening this week.

Declarative Templating and Custom Elements

Right on the heels of the declarative DOM Parts discussion was a broader discussion on declarative templating and fully declarative custom elements. Similar to DOM parts, there was a lot of discussion that didn’t result in consensus around any particular proposal. Rather, the group resolved to compile a list of all open questions and related standards that need to be considered as part of the design of this feature. Since it is so complex and ties everything together, there are many details that need to be considered.

As is often the case when exploring one standard, other missing platform features or behaviors become apparent. During the discussion of how to connect a declarative custom element to its inline script, the problem that there was no way to get the default export of a script tag surfaced. The group discussed that this needed to be resolved more broadly and would be a benefit beyond that of declarative custom elements.

Scoped Custom Element Registries

Since Edge has started to implement this in Chromium, the discussion started with a few questions that have emerged as part of that process. The biggest open question was how this would integrate with Declarative Shadow DOM (DSD). The group resolved that there should be a customregistry attribute added that creates a null registry so that the elements in the DSD are not upgraded by the browser until a registry is explicitly assigned later through the customElements property. The shadow root would also have a scopedregistry boolean property set to true when this was the case so that developers can distinguish between a root that is waiting for its registry and one that simply has an empty registry.

IMPORTANT: Discussion on the final names of these attributes and properties is still ongoing and has changed a bit since the meeting. For example, the latest proposal PR uses the name shadowrootregistry instead of customregistry and hasScopedRegistry instead of scopedregistry. However, the main design approach and open questions are resolved.

Declarative CSS Module Scripts

Declarative scenarios seemed to be the main theme of the first day of the conference. So, the day wrapped up with a discussion of how to enable CSS Module Scripts to be used in a declarative way that would not only integrate with Declarative Shadow DOM (DSD) but also with the broader module loader infrastructure. Two different approaches were discussed:

  • A new type attribute on the style element could be introduced with a value of adopted-css. Then a DSD could use an adopted-styles attribute to reference the id of the adopted-css style module.
  • Alternatively, a more general-purpose solution could be developed that would enable any script tag to have a specifier attribute. This attribute would effectively plug the script into the JS module loader infrastructure so that it could be imported anywhere via the specifier. A css-modules attribute on the DSD could then be used to list out any CSS Script Module that it wanted to import, regardless of whether it was declared inline or not.

The group seemed to favor the second option, which would look like this in practice:

<script type="css" specifier="/bar.css">
:host {
color: red
}
</script>

<my-element>
<template shadowroot="open" css-modules="/foo.css /bar.css">
<script type="css" specifier="/foo.css">
:host {
color: red
}
</script>
<!-- ... -->
</template>
</my-element>

<my-element>
<template shadowroot="open" css-modules="/foo.css /bar.css">
<!-- ... -->
</template>
</my-element>

The group reached consensus to discuss this second option further with implementors in order to determine if this was a viable path.

Day 2

ARIA Mixin & Cross Root ARIA

The second day of the event began with Nolan Lawson of Salesforce providing an overview of several ARIA proposals, each with their own strengths and weaknesses.

Proposal 1 — ARIAMixin Element References

<label>Foo</label>
<x-input>
#shadow-root
<input type="text">
</x-input>
input.ariaLabelledByElements = [label]

This ability to manually link a label from outside of the root to an element inside the root is available today behind a flag in Chromium. This solves the basic use case but has a few downsides:

  • There is no declarative syntax.
  • It only works for shadow-including ancestor elements. (i.e. you cannot link an element in a sibling shadow dom)
  • There are potential memory leeks if the links are not carefully managed.

Proposal 2 — ElementInternals with ARIAMixin

<label>Foo</label>
<x-input></x-input>
class XInput extends HTMLElement {
constructor() {
super()
const internals = this.attachInternals()
internals.role = "textbox"
internals.ariaLabelledByElements = [label]
}
}

This API is available in the Safari Technology Preview but is only partially implemented in Chromium. Unlike Proposal 1, these links can reach across any Shadow DOM boundary. However, there are some downsides:

  • There is no declarative syntax.
  • It only works with custom elements, not with arbitrary Shadow DOM.
  • It is only useful for default relationships.

Option 3 — Cross Root ARIA

<label id="foo">Foo</label>
<x-input aria-labelledby="foo">
<template shadowroot="closed"
shadowrootdelegatesariaattributes="aria-labelledby">
<input type="text"
delegatedariaattributes="aria-labelledby">
</template>
</x-input>

This API is being explored by Edge and has many advantages. It’s fully declarative, highly flexible, and supports import/export of elements. However, it also has a few disadvantages:

  • It’s quite verbose.
  • This only supports ARIA and won’t work with other attributes, such as for.
  • It suffers from the bottleneck effect.

There was a lot of discussion over these proposals. Most notable was the discussion that there might be a broader feature design that could handle all cross-root scenarios, not just labels or ARIA attributes. On that note, an additional proposal was called out as something the group should explore, likely in addition to the above options: Encapsulation-preserving IDL Element reference attributes. It was resolved to schedule additional breakout sessions to discuss the options and designs further.

Reaction Callbacks for Child Node Changes

Since it’s common for a parent element to need to react to the addition or removal of child nodes, the possibility of a new childrenChangedCallback was discussed. The main debate was whether the callback should fire only when the child nodes finished parsing or serve as a more general callback that would fire any time a child node was added or removed. The consensus was that some sort of hook was needed.

During the course of discussion, it was realized that MutationObserver already provides a way to detect child node add/remove. It was then suggested that the MutationRecord type be extended with a new boolean property, closingTagParsed . Then developers would be able to track all child node changes along with the indication of whether the change happened before or after parsing of the parent element’s end tag.

Enhancements to Form-Associated Custom Elements

Form-Associated Custom Elements (FACE) are already supported by all browsers, but there were a few details that the group wanted to make sure were consistent. In particular, labeling. After some discussion, the group reached agreement that all FACE should be labelable. In fact, upon further research, the spec already indicated this. However, it appears that to make this work properly and consistently, additional updates to the interactivity specification were needed (this detail is a bit esoteric, but important to ensuring cross-browser compatibility).

Web Component Pain Points

While a ton of work has gone into shipping Web Components and related specs over the years, there are still pain points for various scenarios. The group took time to rank and discuss the top reported issues.

Lack of Declarative Support for FACE and ElementIInternals

A common theme over both days was that of shoring up support for declarative APIs. Along those lines, a lack of mechanisms for representing ElementInternals and FACE APIs in a declarative way was top of mind for many participants. Some of these issues had already been discussed during the event, with a resolution to continue discussion in future breakout sessions.

Theming and Open Styling

There was a lot of debate over the request to allow for a mechanism for external styles to affect Shadow DOM content. Technically, adoptedStyleSheets on open Shadow DOM enables this today, on a per instance basis. Some consumers of components want the power to override the internal styles of 3rd-party Web Components across the board though. At the same time, component authors often want to prevent blanket overrides, only allowing customization through design tokens and other public APIs they define. Ultimately, it was resolved that a dedicated breakout session was needed to discuss this further.

Lazy Components

It was universally agreed upon that an API for lazy loading of custom elements was needed. There are still many open questions though. For example, should a lazy loaded element be loaded based on presence in the DOM (MutationObserver semantics) or based on its visibility in the viewport (IntersectionObserver semantics) or something else? No consensus was reached at this time.

Styling empty slots

Today, if you want to write a Web Component with slots that needs to style itself differently based on whether something is slotted or not, it’s a surprising amount of work. Fortunately, the CSS Working Group recently resolved to add a pseudo class for slots with content. The proposal is forthcoming.

Async rendering and observation of measurement

Often when building components with libraries like FAST and Lit, a component developer wants to set a number of stateful properties and then may need to measure the DOM. For performance reasons, these libraries often reflect updates to the DOM using asynchronous techniques. As a result, measuring the DOM immediately after setting properties can yield incorrect measurements. Today, each library has its own mechanism to defer code execution until after it has flushed changes to the DOM, but it would be nice to have a library-independent way to hook into the platform, particularly for measurement.

There was a lot of discussion on this, particularly with implementors indicating how complex this would be to handle since this timing isn’t specified and is often handled differently by each implementation, especially to achieve various performance optimizations. Nothing was resolved at this time, but the CSS Working Group is exploring how they may be able to reach better timing consistency across implementations in the future.

Issue Triage

Rounding out the conference, WCCG members spent time triaging all open issues on the Shadow DOM and Custom Element GitHub repos. It was nice to see a number of issues closed as resolved and to link many open issues to the discussions that had taken place over the two-day event.

Wrapping Up

The Spring WCCG meetup resulted in many great conversations, including a number of resolutions that will take various proposals forward to the next stage. The work on Web Components remains highly active with support from all vendors and great enthusiasm from the community and library authors. I hope this window into the standards-making process has been generally insightful for you and that you found something here to be excited about in the future of the Web Platform.

If you enjoyed this look into Web Components, you might want to check out my Web Component Engineering course. I’d also love it if you would subscribe to this blog, subscribe to my YouTube channel, or follow me on twitter. Your support greatly helps me continue writing and bringing this kind of content to the broader community. Thank you!

--

--

EisenbergEffect

W3C, WICG & TC39 contributor focused on Web Standards, UI Architecture & Engineering Culture. Former Technical Fellow, Principal Architect & VP-level tech lead.