- From: Rajdeep Chandra <notifications@github.com>
- Date: Wed, 27 May 2026 02:43:58 -0700
- To: WICG/webcomponents <webcomponents@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <WICG/webcomponents/issues/1111/4553372425@github.com>
Rajdeepc left a comment (WICG/webcomponents#1111)
We maintain [Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/). `sp-checkbox` is already cited in the Phase 1 explainer. We have several Phase 2 use cases where we need to reference **specific, individual** elements inside shadow DOM.
---
### 1. `aria-activedescendant` across nested shadow roots (combobox, picker)
Our `<sp-combobox>` has an `<input role="combobox">` inside its shadow DOM. The listbox options are `<sp-menu-item>` elements rendered inside `<sp-menu>`'s shadow DOM (a separate component). Today, `aria-activedescendant` on the input must point at individual option elements, but IDs are scoped per shadow root.
```html
<!-- Light DOM (consumer) -->
<sp-combobox label="Country">
<sp-menu-item value="us">United States</sp-menu-item>
<sp-menu-item value="ca">Canada</sp-menu-item>
</sp-combobox>
```
```html
<!-- sp-combobox shadow DOM (simplified) -->
<input
role="combobox"
aria-activedescendant="us"
aria-controls="listbox-menu"
/>
<sp-menu id="listbox-menu" role="listbox">
<!-- options rendered here, inside sp-menu's shadow root -->
<sp-menu-item id="us" aria-selected="true">United States</sp-menu-item>
<sp-menu-item id="ca">Canada</sp-menu-item>
</sp-menu>
```
**Workaround today:** We render all options inside the combobox's own shadow root (duplicating them from slotted content) so the IDs resolve within one tree. For our picker component (`<sp-picker>`, similar to a `<select>` replacement), we avoided `aria-activedescendant` entirely and use roving `tabindex` instead — moving actual DOM focus between the trigger button and menu items — because the menu lives in an overlay with its own shadow root. This is a worse UX for the combobox pattern (focus should stay on the input while visually tracking the active option) but was the only reliable option.
**What Phase 2 would solve:** If each `<sp-menu-item>` could be individually referenced from outside its shadow root, we could use `aria-activedescendant` as intended — focus stays on the input, the active option is referenced by ID across shadow boundaries.
---
### 2. `aria-labelledby` degradation across shadow roots (field labels)
Our `<sp-field-label>` component implements `<label for="...">` semantics for custom elements. When the label and the target control share the same shadow root, we use `aria-labelledby` (an ID reference). When they're in different roots, **we fall back to copying the label text into `aria-label`** — losing the semantic relationship:
```js
// Simplified from our FieldLabel implementation
if (targetRoot === labelRoot) {
// Same tree — use the proper ID-based relationship
target.setAttribute('aria-labelledby', this.id);
} else {
// Cross-root — degrade to a text copy, losing the relationship
target.setAttribute('aria-label', this.labelText);
}
```
**What's lost:** The `aria-labelledby` relationship gives assistive technology a live binding — if the label text changes, the accessible name updates automatically. Our `aria-label` fallback is a static string snapshot that requires manual synchronization. It also breaks the "click the label to focus the input" pattern unless we add JavaScript click handlers (which we do).
**What Phase 2 would solve:** Fine-grained `aria-labelledby` into shadow DOM would let the label reference the inner `<input>` (or the host forward the reference to it) without losing the live relationship.
---
### 3. `aria-describedby` across shadow roots (help text, tooltips)
Our form components have help text (`<sp-help-text>`) that provides validation messages and descriptions. The help text content is slotted into the component, but the `<input>` it describes lives inside the component's shadow DOM. We need `aria-describedby` on the input to point at the help text element.
```html
<!-- Light DOM -->
<sp-textfield label="Email">
<sp-help-text slot="help-text">Enter your work email</sp-help-text>
</sp-textfield>
```
**Workaround today:** Our `HelpTextManager` generates synthetic IDs, assigns them to slotted elements, and wires `aria-describedby` on the host — because the host and slotted content share the same tree. But this only works because we keep everything in one root. The `<input>` inside the shadow DOM cannot reference the slotted help text by ID, so the `aria-describedby` goes on the host rather than the semantically correct inner element.
**Tooltip case:** For our 2nd-gen `<swc-tooltip>`, we've planned to use `Element.ariaDescribedByElements` (element references) specifically because string `aria-describedby` on the trigger host cannot reference an element across a shadow boundary. Our button-like triggers render a semantic `<button>` inside their shadow DOM — the AT-facing interactive element is the inner button, not the host — so we need to set the description on that inner element, pointing at the tooltip which lives outside it.
**What Phase 2 would solve:** Being able to reference the tooltip from the inner `<button>` across the shadow boundary, or having the host forward `aria-describedby` to the correct inner element, would let us use standard ID-based associations instead of requiring the newer element-reference APIs.
--
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1111#issuecomment-4553372425
You are receiving this because you are subscribed to this thread.
Message ID: <WICG/webcomponents/issues/1111/4553372425@github.com>
Received on Wednesday, 27 May 2026 09:44:02 UTC