Web Components 2024 Winter Update
Last week, 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. In this post, I’ll go over the wide variety of topics that were covered.
Day 1
Declarative Shadow DOM (DSD)
Day 1 of the event started out with some great news. Firefox is on track to ship DSD in version 123 or 124. Based on the Firefox release calendar, that means we’ll see full cross-browser support for this paradigm-changing feature in February or March of this year.
UPDATE: Firefox has greenlit DSD for version 123, going out February 20!
If you aren’t familiar with DSD, it’s a new standard that enables creating Shadow DOM in HTML without writing any JavaScript. Declarative Shadow DOM enables a bunch of interesting scenarios:
- Pseudo custom components with fully encapsulated styles and slot-based composition, without JavaScript.
- Out of order, deferred content streaming with no need for JavaScript.
- Improved declarative separation of HTML intended for semantic vs. presentation purposes.
- A standard output for server-rendered Web Components.
- An improved migration path from global CSS techniques to more performant, modular CSS.
- An improved migration path to more modern, component-oriented HTML architectures.
- New progressive enhancement approaches.
If you are interested in learning more about Shadow DOM, including the new declarative capabilities, I’ve got an entire module in my Web Component Engineering course dedicated to the topic.
CSS Slot Content Detection
When building Web Components or using Shadow DOM, there’s often a desire to alter styles based on whether something is slotted into a particular location. For example, imagine a button component that needs to alter its internal spacing based on whether or not an image was slotted into the button. Today, this is accomplished by adding an event listener to the slotchange
event and altering the styles based on the assignedNodes()
.
Because this scenario is so common, the Web Components CG has been working with the CSS WG and browser vendors to extend CSS with a new fully declarative mechanism that enables styling based on slotted state.
There are various approaches that could be taken to solve this problem in CSS, such as using a combinator or a pseudo-selector. While a combinator would be more powerful, it may also cause significant complexity and even performance problems. In light of this, the group seemed to favor the simpler approach of pursuing a pseudo-selector that is very targeted in scope, which could be shipped faster and with less risk.
The group is following up in W3C on next steps to get an official spec in place following these discussions. If you want to learn more about today’s techniques for this scenario, the Form Integration module of my Web Component Engineering course covers this in detail.
Scoped Element Registries
Today, all custom elements are defined globally in HTML. While this works fine for many sites and small to medium applications, it becomes a challenge in larger modular apps, mini-apps, micro-frontends, and other more complex scenarios. To address this need, the ability to create scoped registries of components and associate them with Shadow DOM has been proposed.
Fortunately, towards the end of last year, the scoped registry feature was successfully prototyped in Chromium. This enabled a few open questions and important details to be worked out. The CG agreed that the next steps are to update the spec based on lessons learned from the prototype and build out further prototypes in other browsers. We’re actively working on getting together the people to make this happen, with volunteers already stepping forward to implement cross browser.
If you are interested in learning more about custom element registries, as well as best practices for exporting, registering, and distributing Web Components, I’ve got quite a bit of content available here, where I walk through creating a Web Component-based design system.
ARIA Mixin
I’m very happy to report that with the release of Firefox 119 in October 2023, every major browser has now shipped support for ARIA Mixin string reflection via ElementInternals
. This means that string-based ARIA attributes can be set through ElementInternals
without needing to create attributes manually on the element’s host, enabling cleaner HTML and the ability for the component consumer to override default ARIA settings with their own.
ASIDE: For more information on using ARIA Mixin, as well as how to write your own polyfill for older browsers, see my course. 😆
Cross Shadow Root ARIA
One of the things that’s not possible out-of-the-box with custom elements is connecting ID references across shadow roots. For example, if you are building a custom input control and you want a label outside of the element to be able to target an input inside of the element’s shadow DOM. Over the years, there have been a number of proposals for how to improve this, but this year I saw what I think is the most promising proposal so far, one that balances ease of use with power. The new proposal is called Reference Delegate for Cross-root ARIA.
Using the proposed feature, a shadow root could specify an element that all ID refs should be delegated to. So, when a label’s for
attribute references the host element, it will delegate through to the shadow root’s declared reference delegate. Here’s some code that shows the most basic scenario, using the imperative API:
<script>
customElements.define("fancy-input",
class FancyInput extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `<input id="real-input" />`;
this.shadowRoot.referenceDelegate = "real-input"; // magic!
}
}
);
</script>
<!--This label will target the internal real-input of fancy-input -->
<label for="fancy-input">Fancy input</label>
<fancy-input id="fancy-input"></fancy-input>
There’s also a no JS, declarative version of this for use with DSD:
<label for="fancy-input">Fancy input</label>
<fancy-input id="fancy-input">
<template shadowrootmode="open"
shadowrootreferencedelegate="real-input">
<input id="real-input" />
</template>
</fancy-input>
One of the nicest details of the feature design is that it works with any attribute that refers to another element by ID string. Here’s the list as outlined in the RFC:
aria-activedescendant
aria-controls
aria-describedby
aria-details
aria-errormessage
aria-flowto
aria-labelledby
aria-owns
for
form
list
popovertarget
invoketarget
interesttarget
headers
itemref
The attendees seemed to be pretty pleased with this proposal, particularly since it makes the most common scenario very simple and works both imperatively and declaratively in an intuitive way. There was much more discussion around complex scenarios, which the RFC proposes to solve via the referenceDelegateMap
. This allows mapping different ID refs to different elements. Here’s an example of using the declarative API for that:
<input role="combobox"
aria-controls="fancy-listbox"
aria-activedescendant="fancy-listbox" />
<fancy-listbox id="fancy-listbox">
<template shadowrootmode="open"
shadowrootreferencedelegatemap="aria-controls: real-listbox,
aria-activedescendant: option-1">
<div id="real-listbox" role="listbox">
<div id="option-1" role="option">Option 1</div>
<div id="option-2" role="option">Option 2</div>
</div>
</template>
</fancy-listbox>
NOTE: The map can be updated dynamically. So, the mapping from
aria-activedescendant
tooption-1
can be changed based on user interaction.
While this is a bit more complicated than the simple referenceDelegate
it still seemed to me to be a pretty intuitive way to handle the situation. I’ll be keeping an eye on this proposal, hoping it comes to browsers soon.
Day 2
Open Stylable
The term “open stylable” loosely refers to a collection of use cases related to enabling global CSS to take effect inside of the Shadow DOM of specific Web Components. This is most often requested by folks who want to use Web Components, but are required to integrate with a global stylesheet, which they often have no control over. But that is not the only scenario.
Broadly speaking, there are ways to handle this today, using a very small amount of JS in combination with Adopted StyleSheets. But there isn’t a standardized or fully declarative mechanism that works without JS.
There was a lot of discussion within the group on this topic. The biggest challenge seemed to be lack of clarity surrounding use cases. Over the years, this issue has been raised several times, but often without clear real-world use cases, or with wildly different needs. There have often been differences in scenarios, which then lead to subtlety different proposals for how to address the issue, which prevented any sort of consensus in the CG or community…and of course that resulted in no standard being established.
To try to move this in a more productive direction, the CG has asked for the submission of classic user stories, detached from any specific technical solution. In other words, rather than making a specific feature request, the group needs to understand what folks are trying to accomplish. It may be that the solution actually involves adding more than one feature in the end, but it’s hard to see that without the real stories to drive the overall platform design.
Container Style Queries
CSS Container Size Queries have been implemented in all the major browsers today. But only Chromium has shipped CSS Container Style Queries for custom properties. During the CG, time was taken to make sure that each of the browsers was on board with custom property-based container style queries. There was strong consensus. So, with the spec written, one shipped implementation, and cross browser consensus, we’re now just waiting for the additional Firefox and WebKit implementations to be completed and shipped.
ASIDE: If you want to learn more about CSS Container Size and Style queries, particularly in the context of Web Components, check out my Web Component Engineering course. Dang, there’s a lot of Web Standards goodness packed into that course…
CSS Module Scripts, Imports, and the @sheet Proposal
The basic CSS Module Scripts feature has been standardized and is moving forward. If you aren’t familiar with that, here’s an example from my Web Component engineering course:
import styles from "./ui-card.css" with { type: "css" };
const template = `
<slot></slot>
`;
export class UICard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" }).innerHTML = template;
this.shadowRoot.adoptedStyleSheets.push(styles);
}
}
customElements.define("ui-card", UICard);
Notice how a CSS file can be directly imported using the import attribute “css” type. When that happens, the default export is a CSSStyleSheet
instance which can be passed directly to a shadow root’s adoptedStyleSheets
array.
The Web Components CG is working with the W3C CSS WG to expand CSS imports to enable specifying multiple sheets inside of a single file. This would allow bundling component CSS into a single CSS file while keeping each sheet isolated to the Shadow DOM that it is imported into. To enable this, a @sheet
syntax is being proposed. Here’s what that would look like in CSS and how it could be used in a Web Component.
/* components.css */
@sheet uiCard {
:host {
box-shadow: 0 0 .5rem rgba(0,0,0,0.15);
border: .075rem solid #d8d8d8;
border-radius: .375rem;
}
}
@sheet uiButton {
:host {
...
}
}
import { uiCard as styles } from "./components.css" with { type: "css" };
const template = `
<slot></slot>
`;
export class UICard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" }).innerHTML = template;
this.shadowRoot.adoptedStyleSheets.push(styles);
}
}
customElements.define("ui-card", UICard);
Once this feature is available, Web Component libraries that use this technique to distribute their CSS will not only gain the benefit of improved performance but will also make it much easier for component consumers to customize their styles. Consumers would simply provide their own CSS file to replace the originally distributed file.
NOTE: There is a separate proposal for declaratively importing CSS into a DSD. My hope is that however that feature works out in the end, it will also integrate with the new
@sheet
capability. With that in place, the entire example above could be done without any JavaScript.
HTML Modules and Declarative Custom Elements
With JS Import Attributes enabling the import of JSON and CSS, the group wanted to re-open earlier discussions about the possibility of HTML modules. Ultimately, there was consensus that something like this was needed, but that this might not be the right time yet.
In fact, as discussion proceeded, the group wondered whether it might be better to first explore fully declarative custom elements. There has been huge, long-time interest in this. Perhaps a minimal declarative format could be developed more quickly, and then expanded over time. The group resolved to explore this further, with a note that if a fully declarative format could be standardized, browsers could make further optimizations, particularly around caching binary representations of components and reusing them across page loads. Firefox has some previous experience doing just this with XBL and was very interested.
Wrapping Up
All-in-all, it was a great winter meetup. It was particularly nice to see features like ARIA Mixin shipped cross browser and get confirmation from Firefox that DSD was coming very soon. Discussions around in-progress proposals and standards were productive with a solid set of action items to help move forward. It looks like 2024 is going to be another great year for Web Components!