The Bug: Silent Failure of Cross-Root ARIA Assignments

Setting input.ariaDescribedByElements = [helpText] silently does nothing when helpText resides in a shadow root that is not an ancestor of input's shadow root. The getter returns null or [], no error, no warning. Assistive technology (AT) users get no description.

Why It Happens: The Scope Rule

The spec for reflected element references enforces a scope rule: the target element must be in the same DOM as the source, or in a parent (lighter) DOM. Siblings, cousins, children inside deeper shadow roots are all silently rejected. This was added to prevent leaking shadow DOM internals via the getter.

Example of working assignments:

// Same shadow root: works
input.ariaDescribedByElements = [help];

// Referencing "up" into lighter DOM: works
input.ariaDescribedByElements = [outerHelp];

Example of broken assignment:


  
    
  


  
    Helpful description
  

// Sibling shadow roots: silently fails
input.ariaDescribedByElements = [tip];
// getter returns [], AT sees nothing

The Encapsulation Concern vs. Accessibility Failure

The spec authors feared that if you set el.ariaActiveDescendantElement to a shadow child, then any script with access to el could traverse into the shadow tree via the getter. Alice Boxhall's analysis raised this scenario:

lightEl.ariaActiveDescendantElement = shadowChild;
// Now any script can traverse into the shadow tree
lightEl.ariaActiveDescendantElement.parentElement.appendChild(document.createTextNode("surprise"));

But the chosen solution—silently discarding the setter—trades an encapsulation worry for an accessibility failure. The encapsulation worry has straightforward fixes (e.g., null the getter but keep the internal relationship for AT), while the accessibility failure has no fix from the developer's perspective short of restructuring DOM.

The Getter Problem Has Known Solutions

The spec discussion on whatwg/html#5401 explored multiple options:

  1. Return null from getter, keep internal reference for AT – the "attr-associated element" remains intact for the accessibility tree even though JavaScript can't read it.
  2. Retarget the reference to the shadow host – similar to event retargeting across shadow boundaries. Alice Boxhall formalized this in WICG/aom#195, proposing an opt-in API getAttrAssociatedElement to undo retargeting when the caller has access to the relevant shadow root.
  3. Reference Target – the component explicitly declares which internal element should be exposed.

Alice, who did much of the design work on ARIA element reflection, shared her frustration in a recent discussion:

> "I agree with Nolan's suggestion in the bug that developers should get a warning… supporting the cross-root case was indeed what we hoped the feature would enable; there was push-back from other standards engineers based on the reasoning I explained in the bug, so yeah it is now in something of a semi-broken state, which is very frustrating when so much work went into it… perhaps we would have been better off not trying to ship it at all"

Imperative Means Intentional

When a developer writes input.ariaDescribedByElements = [someNode], they already have both references. They already traversed whatever boundaries stood between them. The assignment is an explicit, deliberate act. As Steve Orvell (Lit team) noted after discussing with Chrome engineers:

> "this was done to hide shadow details, but I think there is room for push back since it's an imperative API and you need to be able to get access to all the nodes in question to use the API."

The encapsulation was already broken the moment you obtained the reference. Having = silently un-break it doesn't restore encapsulation; it just breaks accessibility.

The Priority of Constituencies

The W3C's HTML Design Principles establish a binding hierarchy: users > authors > implementors > specifiers > theoretical purity. The current behavior inverts this. Spec engineers chose to protect encapsulation purity over user access to accessibility features. The constituency harmed (AT users) ranks highest; the constituency served (spec authors concerned about theoretical leaks) ranks lowest.

Closed Shadow Roots Are Rare

Chrome UseCounters show open shadow DOM on ~17.5% of page loads vs closed on ~5.3%. Even that ~5% likely reflects ad-tech widgets and third-party embeds, not web component library usage. No major web component framework defaults to closed shadow roots. Lit, Stencil, FAST, Angular, Svelte, Vue—all default to open. Of 30,000+ Lit components on GitHub, roughly 5-10 use closed shadow roots. eslint-plugin-wc ships a no-closed-shadow-root rule warning that closed roots hinder development.

What Developers Actually Need

A real pattern from the ARIA 1.1 Combobox example with web components:


  #shadow-root
  


  #shadow-root
  <ul>
    
      #shadow-root
      <li>List item</li>
    
  </ul>

The `` needs aria-activedescendant pointing to the active <li>. These shadow roots are siblings. The imperative API was supposed to solve this—it doesn't. Nolan Lawson (Salesforce) documented in WICG/aom#192:

> "I guess for me the question is: 'Why is it acceptable for elements in separate shadow roots to be linked with aria relationships, but only if those shadow roots are in a descendant-ancestor relationship (and only in one direction)?'"

The Fix

  1. Make the setter work. Persist the internal relationship for the accessibility tree regardless of shadow root topology.
  2. Null out the getter if needed. Return null from the JavaScript getter when the referenced element is in a deeper/sibling shadow root. This preserves encapsulation for scripts while letting AT see the relationship.
  3. Warn, don't fail. At minimum, emit a console warning when an assignment is silently scoped away. Nolan Lawson suggested this—developers need to know when their accessibility code doesn't work.
  4. Ship Reference Target too. It's the right long-term solution for the declarative case.

The Cost of Silence

A developer writes:

for (const node of descriptors) {
  el.ariaLabelledByElements = [
    ...el.ariaLabelledByElements,
    node,
  ];
}

This loop silently does nothing for cross-root nodes. AT users get no labels. No error, no warning. The developer assumes it works. It doesn't.

Conclusion

The current spec behavior is a violation of the HTML design principles. It prioritizes theoretical encapsulation over real user needs. The fix is clear: make the imperative API work cross-root, null the getter if needed, and warn on silent failure. Developers should file issues on whatwg/html and push for change.