影子边界会改变语义和 ARIA 的运作方式。主要有两种故障:影子树内缺少语义,以及 ARIA 引用因无法跨越影子根而静默中断。
从语义标记开始:在模板中使用真实的 <button>、<nav>、<ul>,这样大多数语义都会自动生效,并为缺口保留 ElementInternals/ARIA。
通过 ElementInternals ARIA mixin 实现默认语义
通过 ElementInternals 在 JS 中设置自定义元素的角色和 ARIA 状态,而不是将 ARIA 属性写入宿主。这样可以保留组件拥有的默认语义,同时仍允许使用者使用 Light DOM 中的属性替换这些语义。
class ToggleSwitch extends HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
this.#internals.role = 'switch'; // default role, no host attribute needed
this.#internals.ariaChecked = 'false'; // exposed to the accessibility tree
this.addEventListener('click', () => this.toggle());
this.addEventListener('keydown', (e) => {
if (e.key === ' ') { e.preventDefault(); this.toggle(); }
});
}
connectedCallback() {
// Put the host in the tab order on connect (setting tabIndex in the constructor throws on createElement).
if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
}
toggle() {
const on = this.#internals.ariaChecked !== 'true';
this.#internals.ariaChecked = String(on);
}
}
customElements.define('toggle-switch', ToggleSwitch);
这样可以避免使用 ARIA 属性污染 DOM,并防止使用者意外破坏这些属性,同时无障碍树仍能看到正确的角色和状态。
焦点管理和 delegatesFocus
- 当宿主元素本身是互动控件,并通过
ElementInternals承载角色和状态时(如上文中的ToggleSwitch),请将宿主置于connectedCallback内的 Tab 键顺序中(if (!this.hasAttribute('tabindex')) this.tabIndex = 0- 绝不能在构造函数中,否则会在document.createElement上抛出异常),以便焦点落在宿主上,并让辅助技术播报其角色和状态。请勿在静态、非互动组件(例如标注或卡片)的主机上设置tabindex。 - 当组件封装拥有语义的内部可聚焦控件时(或在具有
static formAssociated = true的表单关联自定义元素上),使用delegatesFocus: true附加 shadow root。聚焦宿主(并点击表单关联元素的关联<label>)会将焦点转发到第一个可聚焦的子元素,并且:focus与宿主匹配。请勿将delegatesFocus: true与宿主上的角色/状态结合使用,否则焦点将落在内部元素上,并跳过宿主的状态。 - 对于漫游焦点或将焦点移至新显示的区域,请为目标指定
tabindex="-1"并调用.focus();明确指定可通过编程方式聚焦的目标,而不是依赖于 Tab 键顺序。
跨 shadow root 的 ARIA 关系
- 强制性:由
aria-labelledby、aria-describedby、aria-controls等引用的id仅在引用元素自身的树中解析。Light DOM 中基于id的属性无法指向 shadow 树内的id,反之亦然;此类引用会被静默忽略。 - 如需跨越边界,请使用反映的 ARIA 元素属性(例如
ariaLabelledByElements、ariaDescribedByElements等),这些属性采用直接 JS 元素引用,而不是 ID。它们会向外解析:shadow tree 中的元素可以指向 Light DOM 或任何祖先树(包括插槽内容)中的目标,因此您可以使用 Light DOM 内容标记 shadow 内部控件,而id引用无法表达这一点。 - 它们不会向内解析:元素无法引用后代 shadow tree 中的内容(例如,宿主无法由其自己的 shadow 内容标记)。对于该方向,新兴的参考目标机制是标准方向,但尚未广泛推出。
- 如果无法通过这两种方式表达关系,请将其保留在一个树中(在同一树中呈现标签),或使用
aria-label文本而不是id引用。
后备策略
如果 ElementInternals ARIA mixin 不可用,请改为在宿主上将角色和 ARIA 状态设置为属性 (this.setAttribute('role', 'switch'))。这种方法在任何地方都有效,但代价是将属性放入 DOM 中,而使用者可能会覆盖这些属性。
如果无法使用反映的 ARIA 元素属性,请对同树关系使用基于 id 的 aria-* 属性;跨边界标签没有基于 id 的等效属性,因此请回退到 aria-label 文本。