组件化隔离不自动隔离aria-*属性,其id引用必须唯一且限于同一shadow根内,跨boundary引用无效;role和tabindex不继承,需显式设置;状态同步须匹配dom更新时机,测试需启用shadowdom支持。

组件化隔离对 aria-* 属性的影响
HTML 组件化本身不会自动隔离可访问性(a11y)状态,aria-* 属性是全局生效的,哪怕写在 Shadow DOM 里,如果引用了外部 id(比如 aria-labelledby="header"),而该 id 在 shadow 根外或被重复定义,就会导致屏幕阅读器找不到目标、报错或跳过内容。
常见错误现象:aria-describedby 指向一个在 shadow 根外不存在的 ID;多个组件同时渲染出相同 id(如都用 id="error-message"),触发 WAI-ARIA 规范中“ID 必须唯一”的校验失败,部分读屏器静默忽略或崩溃。
- 组件内所有
id值必须动态生成(例如用crypto.randomUUID()或组件实例 hash),避免硬编码 -
aria-*引用关系(如aria-labelledby、aria-controls)只允许指向同一影子根(shadow root)内的元素,跨 shadow boundary 的引用无效 - 若需跨组件关联(如弹窗与触发按钮),改用事件通信 +
aria-live区域,而非依赖 ID 引用
Shadow DOM 中 role 和 tabindex 的继承行为
Shadow DOM 默认开启 delegatesFocus: true,但 role 不会自动继承,tabindex 也不会穿透。这意味着:即使宿主元素设了 role="button",其 shadow 内部若没显式声明交互语义,键盘焦点仍可能卡住、无法进入,或屏幕阅读器不识别为可操作控件。
典型场景:自定义 <my-button></my-button> 组件内部用 <div> 渲染,没设 <code>role="button" 和 tabindex="0",键盘用户 Tab 进去后无法空格/回车触发,NVDA 或 VoiceOver 直接跳过。
- 任何可交互的自定义组件,其 shadow 内最外层容器必须显式设置
role(如role="button"、role="combobox") - 若组件需要键盘聚焦(非仅靠父级委托),内部需有且仅有一个
tabindex="0"元素,并确保它在 focusable 流中可抵达 - 避免在 shadow 内使用
role="presentation"或aria-hidden="true"封装整个组件结构——这会让整块内容对辅助技术不可见
useEffect / connectedCallback 中可访问性修补的时机问题
在 Lit、Stencil 或原生 Custom Elements 中,常在生命周期钩子(如 connectedCallback)里补全 aria-* 属性。但若 DOM 插入和属性设置不同步(比如异步渲染子组件、延迟挂载 slot 内容),会导致屏幕阅读器读取到“半成品”状态:按钮无 aria-expanded、折叠面板缺 aria-hidden 切换逻辑,引发语音中断或误读。
错误示例:connectedCallback 立即设 this.setAttribute('aria-expanded', 'false'),但实际展开状态由后续 slot 内容是否渲染决定,两者不同步。
- 优先用 attributeChangedCallback 响应状态变更,而非仅靠初始化赋值
- 涉及 slot 内容的可访问性属性(如
aria-labelledby指向 slot 中的标题),应在slotchange事件后重新计算并更新 - 避免在
requestAnimationFrame外直接批量 setAttribute —— 屏幕阅读器可能在 DOM commit 前已开始解析,造成竞态
自动化检测工具为何常漏掉组件化 a11y 崩溃
主流检测工具(如 axe-core、Lighthouse)默认运行在 light DOM 上下文,对 Shadow DOM 的扫描支持有限:axe 需显式启用 shadowDom: true 选项;Lighthouse 2025+ 版本才稳定支持 slot 内容的 aria 引用解析。更麻烦的是,很多崩溃不抛 JS 错误,而是读屏器内部状态错乱(如缓存失效、节点树断裂),这类问题根本不会出现在 console 或 CI 报告里。
- 本地测试必须启用真实屏幕阅读器(NVDA + Firefox / VoiceOver + Safari),不能只信 axe 报告
- CI 中集成 axe 的 shadow DOM 支持:调用
axe.run({ shadowDom: true }),并确保测试环境加载了完整 shadow 树(不是只测 template) - 特别注意动态 ID 生成后,是否在 SSR 和 CSR 场景下保持一致——服务端渲染时生成的 ID 与客户端 hydrate 后不一致,会导致 aria 关联永久失效
组件化隔离真正起作用的地方,不在封装样式或逻辑,而在你是否让每个 shadow 根成为可独立验证的 a11y 单元——ID 唯一性、role 显式声明、状态同步时机、测试上下文,四个点缺一不可。最容易被忽略的是:slot 内容的可访问性责任归属,它既不属于父组件,也不完全属于子组件,而是由 slot 分发机制和 aria 引用链共同决定。











