Can I create an ARIA reference to an element in shadow DOM?
posted on
No.
Element IDs are scoped within a shadow root. An element in light DOM can't reference an element in shadow DOM or the other way around.
class TheHint extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" })
const paragraph = document.createElement("p")
paragraph.id = 'hint'
paragraph.textContent = "Format: DD.MM.YYYY"
this.shadow.append(paragraph)
}
}
customElements.define("the-hint", TheHint)
// => result <the-hint><p id="hint">Format: DD.MM.YYYY</p></the-hint>
That doesn't work:
<label for="date">Birthday</label>
<input type="date" id="date" aria-describedby="hint">
<the-hint>
#shadowRoot
| <p id="hint">
| Format: DD.MM.YYYY
| </p>
#shadowRoot
</the-hint>
IDREF attribute reflection and ARIA mixins can solve this issue, but there are constraints.
ARIA mixins
Every ARIA attribute that refers to other elements by their IDs (aria-labelledby
, aria-describedby
, aria-controls
, etc.) has a corresponding property on DOM elements that you can set or get via JavaScript: For example, ariaDescribedByElements
for aria-describedby
or ariaLabelledByElements
for aria-labelledby
.
What's great about that is that instead of input.setAttribute('aria-describedby', 'hint')
, you can now do input.ariaDescribedByElements = [hint.shadowRoot.querySelector('#hint')]
. You don't reference the id, but the element itself.
That sounds like a solution, but ARIA Mixins are currently only supported in Chrome Canary and WebKit Nightly, and they only work if the referenced element is in the same shadow root as the target element or the referenced element is in a parent, grandparent, or ancestor shadow root of the target element.
The following example won't work because they're not in the same shadow root or in a ancestor relationship:
<label for="date">Birthday</label>
<input type="date" id="date">
<the-hint>
#shadowRoot
| <p id="hint">
| Format: DD.MM.YYYY
| </p>
#shadowRoot
</the-hint>
const input = document.querySelector('#date')
const hint = document.querySelector('the-hint')
input.ariaDescribedByElements = [hint.shadowRoot.querySelector('#hint')]
When support improves, ARIA mixins can solve some problems, but they're not a universal solution. That's the only real option we have at the moment but two proposals could solve this issue: Cross-root ARIA delegation and Cross-root ARIA reflection.
Cross-root ARIA delegation
The idea behind Cross-root ARIA Delegation is that you can set a new option to attachShadow()
called delegatesAriaAttributes
(similar to delegatesFocus
), which enables ARIA attributes set on a custom element to be forwarded to elements inside of its shadow root.
Cross-root ARIA reflection
The idea behind Cross-root ARIA Reflection is that you can set new options to attachShadow()
for ARIA attributes (reflects*
) (similar to delegatesFocus
), which enables you to make elements inside a shadow root available as a target for relationship attributes.
Conclusion
If all your relationships for an element happen exclusively in light DOM or shadow DOM and you don't try to cross boundaries, working with ARIA is not a problem. That's not always possible, though. Without a doubt, there needs to be a solution to that problem. Alice Boxhall described it well.
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.
Alice Boxhall
Resources
- How Shadow DOM and accessibility are in conflict by Alice Boxhall
- Shadow DOM and accessibility: the trouble with ARIA by Nolan Lawson
- Cross-root ARIA Delegation explainer by Nolan Lawson
- Cross-root ARIA Reflection API explainer by Westbrook Johnson