Repository navigation
Live regions that render inside a shadow root are not announced #251
Description
Live regions that render inside a shadow root are not announced
What happens
A live region whose content is rendered inside its own shadow root is never
announced. The shape that hits this is a host element carrying aria-live, with
the message rendered into its shadow root - which is what a web component does
when it renders its own validation feedback.
Reproduced with @guidepup/virtual-screen-reader@0.33.0:
<button id="trigger">Trigger</button>
<div id="host" aria-live="assertive" aria-atomic="false"></div>
<script type="module">
const host = document.getElementById("host");
const shadowRoot = host.attachShadow({ mode: "open" });
document.getElementById("trigger").addEventListener("click", () => {
shadowRoot.innerHTML = "<span>Please enter a valid IBAN.</span>";
});
</script>await virtual.start({ container: document.body });
document.getElementById("trigger").focus();
await virtual.act();
// expected
["document", "button, Trigger", "assertive: Please enter a valid IBAN."]
// actual
["document", "button, Trigger"]The same region in the light DOM is announced correctly.
Why
Three things, each of which alone is enough to lose the announcement:
- The observer never sees the shadow root.
observeDOMobserves the
container withsubtree: true, and aMutationObserverdoes not cross shadow
boundaries. - A shadow root mutation target resolves to
undefined.
getElementFromNodeisisElement(node) ? node : node.parentElement!.
Rendering into a shadow root replaces the children of theShadowRootitself,
and aShadowRoothas noparentElement. - The walk up cannot leave the shadow root.
getLiveRegionAttributeswalks
up withparentElementonly, so thearia-liveattribute on the host is
unreachable from a node inside the shadow root.
Measured while fixing it: even after observing shadow roots, a mutation whose
target is the ShadowRoot still does not reach the announcement, because of (2).
Suggested fix
A PR follows:
observeDOMobserves shadow roots too, and wrapsattachShadowso shadow
roots attached later are covered as they are created - a shadow root that is
already populated when observation starts never replays those changes.getElementFromNodemaps aShadowRoottarget to its host.- the ancestor walk crosses the shadow boundary (
assignedSlot ?? parentElement ?? getRootNode().host).
Context
Found while adding screen reader tests for @lion/ui (ing-bank/lion):
<lion-input-iban> renders its invalid message in the shadow root of
lion-validation-feedback, which has aria-live="assertive". NVDA and
VoiceOver announce it; the virtual reader did not, so the tests could not assert
on the message locally.
Minor observation while testing: when a live region produces both an additions
phrase and a text phrase, they are joined as "Error,, Please enter a valid IBAN." - the duplicate comma comes from joining the phrase parts, which is
cosmetic but ends up in spokenPhraseLog.
Activity
cmorten commentedon Oct 15, 2026
Potential duplicate of #182, though the emphasis here firmly on live region support whereas the prior on the tree (but it does reference the observe behaviours as well)
Need to review whether #239 might solve this already / what (if any) differences in approaches.
There is also #183 which the original author closed, but seemed reasonably close at the time.