delegatesfocus 不解决 tab 导航,仅响应点击/脚本聚焦;需宿主设 tabindex="0" 并在 focusin 中手动聚焦内部元素,配合防重入标记和嵌套层逐级处理,同时同步视觉与无障碍状态。

delegatesFocus 不解决键盘 Tab 导航,只响应鼠标点击——这是最常被误解的起点。它对 Tab 键顺序完全无影响,启用后焦点依然会在 Shadow DOM 宿主边界中断,除非你额外处理。
为什么 Tab 键进不去 Shadow DOM 内部
浏览器原生 Tab 流只遍历“可聚焦的宿主元素”,不会自动穿透 Shadow boundary。即使你写了 attachShadow({ mode: 'open', delegatesFocus: true }),Tab 按下时焦点仍停在宿主标签上(比如 <my-input></my-input>),而非内部的 <input>。
- 宿主元素必须显式设
tabindex="0"或tabindex="-1",否则它本身不在 Tab 顺序中,根本轮不到它“被 Tab 到” -
delegatesFocus只在点击/触摸/脚本调用.focus()时触发委托,和Tab无关 - 若宿主有
tabindex="0",用户按Tab进入的是宿主,不是内部控件;此时需监听focusin事件并手动跳转
让 Tab 连续进入内部元素的实操写法
真正让键盘用户从一个字段顺滑跳到下一个,靠的是宿主可聚焦 + 手动接管焦点流:
- 宿主元素加
tabindex="0"(不可省略,否则 Tab 根本不会停在这里) - 在宿主的
focusin事件里,查找到 shadow 内第一个可聚焦元素并调用.focus() - 为避免 Safari 中的 focus 循环,加个防重入标记:
if (!this._focusedInside) { this._focusedInside = true; this.shadowRoot.querySelector('input, button').focus(); } - 在内部元素
blur时清除标记:this._focusedInside = false
注意:不要依赖 delegatesFocus 替代这一步——它不触发 focusin,也不保证 Tab 后的行为。
嵌套 Shadow DOM 中的焦点链断裂问题
多层自定义组件(如 <form-card></form-card> → <field-group></field-group> → <my-input></my-input>)会让焦点路径变脆弱:
- 每一层宿主都必须设
tabindex="-1"(或"0"),否则Tab流在某一层就断了 - 每一层都要监听自己的
focusin,并主动把焦点推给下一层的首个可聚焦节点 - 如果某层 shadow 内没有可聚焦元素(比如 slot 还没渲染完),
querySelector返回null,需 fallback 到setTimeout重试或抛出警告 - 避免在
connectedCallback中直接调用.focus()—— 此时 shadow 内容可能尚未挂载完成
视觉反馈与无障碍状态同步不能只靠 :host([focused])
宿主元素自身永远不会获得 :focus 状态,:focus-within 虽能匹配,但不等于你能在 CSS 中直接用 :host(:focus) 做样式:
- 必须在 shadow 内部元素上监听
focus和blur,再通过this.toggleAttribute('focused', true/false)更新宿主属性 -
:host([focused])是唯一可靠的宿主级聚焦样式钩子,:host(:focus)永远不匹配 - 屏幕阅读器需要语义,所以宿主应带
role="group"或对应 role,并用aria-describedby关联说明文字 - 若内部是
<input type="search">,宿主最好同步设置aria-label,否则 SR 只读 “custom element”
最易忽略的一点:焦点委托行为只在用户主动交互时生效,服务端渲染或 SSR 后 JS 未加载完成时,整个逻辑是失效的——必须有降级方案,比如初始渲染时让内部 <input> 带 autofocus(仅首次),或通过 data-focusable 属性做静态 fallback。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











