alice's blog

How Shadow DOM and accessibility are in conflict

Shadow DOM allows web developers to create and use components which encapsulate their internals. Like encapsulation in any other programming context, being able to hide the implementation details of an HTML component has many benefits to both developers using the component in their web pages, and the developers who author and maintain the component. However, there is one major way that, to date, Shadow DOM's encapsulation mechanism is in conflict with techniques authors use to provide an accessible user experience.

Shadow DOM encapsulation essentially makes children of a shadow root "private" to any siblings or ancestors of the shadow host. This means that any HTML feature which creates a relationship between elements can't work when a relationship needs to be expressed between an element within a shadow root and one outside of it.

This has some technical reasons; in particular, element IDs are scoped within a shadow root, so a reference from outside of shadow root can't refer to an element with that ID inside a shadow root. However, it's also a logical quandary: if the elements inside the shadow root are implementation details which are intentionally opaque from the point of view of any code outside the component, how can they also be a part of a semantic association mediated by code?

Semantic relationships and accessibility #

What does it mean for an element to have a semantic relationship with another element?

Probably the most easily familiar example of a semantic association between one element and another is the relationship between an <input> and a <label>. By either placing the <input> between the opening and closing tags of the <label>, or using the for attribute on the <label> element pointing to the ID of the <input> element, we can express that there is a relationship between the two elements. The <label> provides a label for the <input>, and the <input> is labelled by [1] the <label> .

The association between an <input> and a <label> makes absolutely no difference to how those elements are rendered. If I put a <label> (or even just some text) right next to an <input>, a user who can see both will easily intuit that the label text is associated with the input field. And the label also makes no difference to what data is submitted with the associated <form>.

By creating the semantic association, essentially what we're doing is explicitly expressing in code what is implicitly expressed by the way the elements relate to each other visually.

In the case of <label> and <input>, we do get a nice little bonus in that it makes the entire contents of the <label> a click/tap target for the <input>. So if you're labelling a checkbox, for example, you're making it easier for users of pointer-based devices (i.e. most users) to actually use the checkbox itself.

A less easily appreciated effect of creating a semantic relationship between <label> and <input> is the way it affects the experience of Assistive Technogy (AT) users. Assistive technologies are software and hardware tools which can augment a user experience to meet the needs of people with certain disabilities.

For example, a blind person might use a refreshable braille display which consumes information about an application's user interface via an assistive technology API. The braille display can display a single line of text (sometimes up to 80 characters, but more often 20-40), and the user interface the user is interacting with needs to be expressed in a textual way in order for it to be useful to them.

This is achieved by generating a textual, descriptive representation of a particular UI element. So, for example, if we have the following HTML:

<label for="agree">I agree</label>
<input id="agree" type="checkbox">

- then a braille display might allow a user to interact with it when it has keyboard focus by displaying a braille version of the string I agree (x) tck bx. This concisely expresses three critical things:

In this case, the label of the checkbox can reliably be part of the textual representation because of the semantic association between the <label> and the <input type="checkbox">.

Semantic relationships and Shadow DOM #

For more complicated custom elements, the types of relationships expressed go beyond a <label>/<input> relationship. In these cases, ARIA attributes provide a larger vocabulary of relationships which can be expressed.

An accessible combobox involves a number of ARIA relationship attributes. It's a good example of the type of complex component that can take a lot of subtle work to get right, making it a good candidate for a reusable component [2].

One way this might look with Shadow DOM is that the <input> might be tucked away inside a shadow root with some logic, extra elements for presentation, and so on, while the autocomplete options might be provided by the author of the page.

<!-- I'll use #shadowRoot and #/shadowroot to denote the beginning and end of 
the shadow root node, and | (vertical bars) to indicate which nodes are
within the shadow root. -->

<custom-autocomplete>
#shadowRoot
| <input id="innerInput" role="combobox" aria-autocomplete="list"
| aria-expanded="false" aria-controls="autocompleteOptions">

| <div role="listbox" id="autocompleteOptions">
| <slot>
| <!-- author-provided options will be slotted in here -->
| </slot>
| </div>
#/shadowRoot
<custom-option id="opt1">Cassowary</custom-option>
<custom-option id="opt2">Currawong</custom-option>
<custom-option id="opt3">Emu</custom-option>
<custom-option id="opt4">Ibis</custom-option>
<custom-option id="opt5">Magpie</custom-option>
</custom-autocomplete>

From the point of view of the author using the <custom-autocomplete> this is a pretty good deal: they give it a set of <custom-option>s and the logic encapsulated within the component creates an accessible, encapsulated autocomplete widget with those options.

Well, with a few caveats.

Referring from Shadow DOM outwards #

When a user is interacting with the autocomplete component, keyboard focus will be on the <input> element, so the user can start typing the name of the option they're interested in. When the user wants to select an option from the list, they may use the arrow keys to quickly select the option they're interested in. The currently selected option will be visually indicated, but keyboard focus remains in the text field so that the user can keep typing and refine the options further.

a screenshot of the Google search box with the text 'combobox html' followed by an insertion caret, and autocomplete options beginning 'combobox tkinter', 'combobox c#', 'combobox html', with 'combobox html' highlighted

For AT users, we need to ensure that the AT is aware that an option is "active", so that it can display it to the user in the most appropriate way (such as showing its text on the single line of a braille display), without moving keyboard focus off the <input>.

The ARIA attribute which achieves this is aria-activedescendant. Like the <label>'s for', it creates an association between two elements via the ID of the element being referred to; for example:

<!-- this is an extra-simplified example! -->
<input role="combobox" aria-expanded="true" aria-activedescendant="option2">
<ul role="listbox">
<li role="option" id="option1">Hawksbill</li>
<li role="option" id="option2">Leatherback</li>
<li role="option" id="option3">Loggerhead</li>
</ul>

So, in this case, when keyboard focus is on the <input>, the AT will place its "virtual cursor" [3] on the second option, showing the user that option in the appropriate manner while allowing the user to continue typing.

If you're familiar with Shadow DOM, or you read the introduction to this article carefully, you can probably see where this falls apart with the Shadow DOM example above:

<custom-autocomplete>
#shadowRoot
| <input id="innerInput" role="combobox" aria-autocomplete="list"
| aria-expanded="true" aria-controls="autocompleteOptions"
| aria-activedescendant="?????">

| <div role="listbox" id="autocompleteOptions">
| <slot>
| <!-- author-provided options will be slotted in here -->
| </slot>
| </div>
#/shadowRoot
<custom-option id="opt1">Cassowary</custom-option>
<custom-option id="opt2">Currawong</custom-option>
<custom-option id="opt3">Emu</custom-option>
<custom-option id="opt4">Ibis</custom-option>
<custom-option id="opt5">Magpie</custom-option>
</custom-autocomplete>

In this case, even though the <custom-option> is outside the shadow root and thus not part of any encapsulation guarantees, those very encapsulation guarantees prevent the <input> from referring to any of the <custom-option>s, because the shadow root creates a new tree. Because of this, the IDs of the <custom-option>s aren't "visible" from within the shadow root, so there's no value for the aria-activedescendant attribute that could refer to one of them.

There is one option available (not yet universally) to the author of the <custom-autocomplete>: using the ariaActiveDescendantElement IDL attribute:

innerInput.ariaActiveDescendantElement = slot.assignedNodes()[2];

This works where the ID fails, because an IDL attribute with type Element can refer to any element that is a descendant of any "shadow-including ancestor" of the element hosting the attribute.

If you haven't had the misfortune of becoming familiar with the way the HTML spec expresses tree-related concepts, what this means in this example is:

So, while this doesn't yet work everywhere, it offers at least a partial solution to this one part of the problem.

Referring into Shadow DOM #

The above example "works" because the <custom-option>s are in the shadow root's "light tree". But what if the <custom-autocomplete> needed to use an option list which was in another shadow root? For example, the option list might encapsulate logic to fetch autocomplete options from a server, or to use a recycler pattern to manage list items.

That might look something like this:

<custom-address-autocomplete>
#shadowRoot
| <input id="innerInput" role="combobox" aria-autocomplete="list"
| aria-expanded="true" aria-controls="autocompleteOptions"
| aria-activedescendant="?????">

| <custom-recycler role="listbox" id="autocompleteOptions">
| #shadowRoot
| | <custom-option id="opt3">221B Baker St</custom-option>
| | <custom-option id="opt1">29 Acacia Road</custom-option>
| | <custom-option id="opt2">724 Evergreen Terrace</custom-option>
| #/shadowRoot
| </custom-recycler>
#/shadowRoot
</custom-address-autocomplete>

In this example, there is no way to create an aria-activedescendant association from the <input> to one of the <custom-option>s. Unlike in the previous example, the <custom-option>s aren't descendants of a shadow-including ancestor of the <input>, so we can't even use the ariaActiveDescendantElement IDL attribute. This was a deliberate choice in the design of the reflection API: if authors made a reference to an element inside a shadow root available on an element outside of it, it essentially makes the encapsulation moot, particularly in the case of a closed shadow root.

The <custom-option>s are effectively an implementation detail of the <custom-recycler>, so logically it makes sense for them to be hidden or private from the <input>. However, they are only an implementation detail as long as you don't need to express a semantic relationship with one of them in code.

To put it another way: the contents of the shadow root is private to its light tree, but not to users. If a user can perceive a relationship between elements in the light tree and the shadow tree, but the author can't express that relationship in code, then the encapsulation provided by Shadow DOM is at odds with the semantics of the page, and so at odds with accessibility. This is a conundrum for Shadow DOM.

Squaring the circle: prior and current work on expressing relationships across shadow roots #

There have been numerous attempts to try to address this glaring gap in Shadow DOM's capabilities.

Element IDL attribute reflection #

As described above, allowing IDL attributes with type Element to express relationships from elements within Shadow DOM to elements in the light tree. At the time of writing, this is implemented in WebKit (available in Safari Technology Preview) and shipping in Blink-based browsers.

Element IDL attribute reflection to allow referring into open shadow roots #

Nolan Lawson wrote a detailed explainer and proposal that Element IDL references should be permitted to refer into open shadow roots.

This idea has been floated several times, and the push-back tends to be along the lines that even the relatively weaker encapsulation guarantees of open shadow roots would be violated by an API of this form. For example, it would allow a non-Shadow DOM aware script to accidentally traverse into a shadow root's children.

That this proposal keeps being arrived at independently, and in particular that it's now being suggested by developers actively working with Shadow DOM, says something about the appeal - it would be straightforward to spec and implement, and would would be as easy to use as the existing IDL attributes.

It might even be possible to mitigate the encapsulation issues, potentially via something like retargeting as is done for event targets.

Even if that could be done, there are still two major downsides with this proposal:

::part()? #

CSS ::parts seem like a very closely related solution: the problem they're solving was that developers wanted to interact with (in this case, style) elements within shadow roots. ::part does this in an encapsulation-preserving way, because ::parts can only be targeted by CSS styles, not by querySelector() which can only return (light tree) descendants of the parent node.

However, ::parts can't be used directly for IDREF attributes without a major change to how IDREF attributes and indeed ::parts work, since IDREF attributes are explicitly based on the id attribute of the related element, and ::parts are only valid for matching CSS selectors, not HTML attributes.

These may be surmountable issues, but they wouldn't be easy to address; it's probably easier to find new solutions inspired by ::part instead.

Explicit import/export of IDs to/from shadow roots #

There was an attempt to design an API based on exportparts, which would allow the import/export of IDs to/from shadow roots.

This had a few issues with being confusing and arduous to use, most particularly that unlike exportparts, the attributes in question would be on the shadow host, meaning that they needed to be added by the author using the custom element, since they would need to ensure that each exported/imported ID was unique in the scope it was exported/imported to.

This means it would be labour-intensive for authors to use, but also that unlike ::part it doesn't have an opt-in step from the custom element; rather, any element with an ID may have its ID exported or imported using this API, regardless of the intended use or audience for the ID. Essentially, it implies that any ID becomes part of the public API for the shadow root.

Cross-root ARIA (and more?) delegation/reflection #

These complementary proposals (Cross-root ARIA Delegation and Cross-root ARIA Reflection) add attributes which allow a custom element:

Cross-root ARIA delegation #

Taking inspiration from delegatesFocus, cross-root ARIA delegation would allow a shadow root to declare that it "delegates" certain attributes from the shadow host to specific elements within the shadow root.

Taking a modified example from the explainer which uses declarative shadow DOM as an illustration:

<span id="description">Description</span>
<x-input aria-label="Name" aria-describedby="description">
<template shadowroot="closed"
shadowrootdelegatesariaattributes="aria-label aria-describedby">

<input id="input"
delegatedariaattributes="aria-label aria-describedby" />

<button delegatedariaattributes="aria-label">Another target</button>
</template>
</x-input>

Note: the API proposed includes both imperative and declarative versions, but the declarative version makes for a more concise example.

This example would result in both the <input> and the <button> having the aria-label from the shadow host ("Name") applied, and the <input> having an aria-describedby relationship with the span adjacent to the <x-input>.

This means that an author using the <x-input> element can simply apply ARIA attributes to the shadow host exactly as they would for a regular <input> element, and have them work as expected. Furthermore, like delegatesFocus, this proposal would work recursively: any number of levels of shadow roots could delegate attributes from a shadow host to the desired target for those attributes.

As shown in the example above, these relationships are intended serialisable using declarative shadow DOM, via attributes on <template>.

Furthermore, this proposal means that author attributes on custom elements can be respected without the author needing to know anything about the custom element internals - as long as the custom element author has correctly predicted what attributes should apply to which internal elements.

Cross-root ARIA reflection #

Cross-root ARIA reflection is complementary to cross-root ARIA delegation. Essentially, it allows a custom element author to "export" elements inside of a shadow root to be available as a target for relationship attributes - much like a ::part.

Modifying an example from the explainer, once again using declarative shadow DOM:

<input aria-controls="options" aria-activedescendent="options">
<x-optlist id="options">
<template shadowroot="open"
shadowrootreflectsariacontrols
shadowrootreflectsariaactivedescendent>

<ul reflectariacontrols>
<x-option id="opt1">221B Baker St</x-option>
<x-option id="opt2" reflectariaactivedescendant>29 Acacia Road</x-option>
<x-option id="opt3">724 Evergreen Terrace</x-option>
</ul>
</template>
</x-foo>

The <input> ends up with an aria-controls relationship with the <ul>, and an aria-activedescendant relationship with the second <li>.

This is complementary to cross-root ARIA delegation in that it allows the creation of relationships in the opposite direction: from outside a shadow root to inside.

Limitations of these APIs #

Bottleneck effect

The main limitation of these APIs is that they can only allow expressing relationships to one element or set of elements for each attribute in each direction. Effectively, it makes the shadow root a bottleneck for these types of relationships.

For example, a shadow host which has an aria-describedby value can create a relationship from any number of elements within its shadow root (and their shadow roots, recursively) to a single list of elements in its tree. If there are multiple elements within its shadow root which all need different cross-root aria-describedby values, these APIs can't achieve that.

Similarly, a shadow host may reflect an element (or several) as a target for a particular attribute, but any element referring to the shadow host for that relationship will create a relationship with the same element(s).

<custom-address id="address">
#shadowRoot
| <div>
| <slot name="labelforstreet">
<label slot="labelforstreet" for="?????">Street</label>
| </slot>
| <input id="street" aria-describedby="?????">
| <slot name="descriptionforstreet">
<span slot="descriptionforstreet" id="streetdescription">
The street address
</span>
| </slot>
| </div>
| <div>
| <slot name="labelforsuburb">
<label slot="labelforsuburb" for="?????">Suburb</label>
| </slot>
| <input id="suburb" aria-describedby="?????">
| <slot name="descriptionforsuburb">
<span slot="descriptionforstreet" id="suburbdescription">
The suburb
</span>
| </slot>
| </div>
#/shadowRoot
<label slot="labelforstreet" for="?????">Street</label>
<span slot="descriptionforstreet" id="streetdescription">The street address</span>
<label slot="labelforsuburb" for="?????">Suburb</label>
<span slot="descriptionforstreet" id="suburbdescription">The suburb</span>
</custom-address>

In this extremely contrived and unlikely example, the two <input>s inside the shadow root each want different <label>s to be able to refer to them, and want to refer to different light tree elements using aria-describedby. Since the delegation/reflection APIs only allow one target or set of targets for each attribute in each direction (inwards or outwards), this could not work with these APIs.

However, this may not be a huge issue in practice, since custom elements tend to act somewhat "atomically" by design.

Proliferation of IDL attributes

Depending on the exact shape of the API, it may result in the addition of multiple IDL attributes per ARIA attribute to certain objects.

This may be fairly straightforwardly avoided, however. The proposal sketched for cross-root ARIA delegation uses token list attributes [4], for example, which means only one attribute needs to be added (plus the extra attribute on <template>/ShadowRoot). Similarly, the proposed exportfor attribute seems to be an analogous version of reflectaria* which takes a token list.

What needs to happen next? #

There is an increasing sense of urgency about these issues - arguably one that's been missing since the inception of Shadow DOM - since Shadow DOM is hovering around a critical adoption threshold. Collectively, we've been arguing for almost a decade about what the optimal solution might look like, with only modest progress in the form of an API that is only now implemented in two browser engines, and not yet shipping in any.

We have a number of promising proposals as to how to solve these problems, but there is still a lot of work to be done to get us to the point where we actually have solutions shipping and able to be used. We need to investigate how well these proposals follow web standard design principles, how feasible they are to ship in browsers, and how well these proposals fit the problems actually being experienced by custom element authors. We need to prototype proposals which meet these requirements, to get them in the hands of developers who can test them in the context of their actual code. And, we need to navigate the standards process and ensure that we get to the stage of having as many browser engines as possible shipping whatever APIs will meet the needs of both developers and users.

Thank you! #

Thank you to Manuel Rego, Brian Kardell, Eric Meyer and Sarah Higley for reading drafts of this post, providing early feedback and contributing ideas in discussions. Thank you to Westbrook Johnson for the recycler list example. Thank you to Ana Rute Mendes for setting up this blog for me. And thank you to everyone who is still working on or has worked on this problem!


[1] Since I'm writing this as an Australian, this is the natural way for me to spell "labelled", but it's also the way the corresponding ARIA attribute is spelled - much to the annoyance of anyone used to American spellings.

[2] The examples shown here, and in the ARIA Authoring Practices Guide, are not production-ready code, which often takes into account extra considerations over and above those discussed here.

[3] The virtual cursor typically follows keyboard focus, but the user may move it themselves in order to consume non-interactive content, or the AT may move it in cases like this.

[4] <template shadworootdelegatesariaattributes="aria-labelledby, aria-describedby"> rather than <template shadowrootdelegatearialabelledby shadowrootdelegatesariadescribedby>