绝大多数 UI 组件应使用 "open" 模式,便于调试、测试和主题定制;样式须内联至 <style> 标签,通过 :host 和 CSS 变量实现可控穿透,且 attachShadow() 前需清空 innerHTML。

Shadow DOM 的 mode 选 "closed" 还是 "open"?
绝大多数 UI 组件该用 "open"。它允许外部 JS 通过 element.shadowRoot 访问内部结构,方便调试、自动化测试和动态注入(比如 Storybook 预览)。而 "closed" 会屏蔽 shadowRoot 属性,让 element.shadowRoot 返回 null —— 看似更“封装”,实则断了开发者调试链路,也阻碍了合理的样式覆盖(比如主题切换时需注入全局 CSS 变量),目前主流组件库(如 Lit、Shoelace)默认都用 "open"。
样式怎么进 Shadow DOM?内联 <style> 是唯一可靠方式
Shadow DOM 的样式隔离是硬隔离:外部 CSS 选择器进不去,@import 和 @layer 在 shadow 中无效,link 标签加载的外部 CSS 也不会生效。唯一稳定的方式是把样式字符串直接写进 <style> 标签里,再 append 到 shadowRoot:
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; }
.btn { background: var(--btn-bg, #007bff); }
</style>
<button class="btn"><slot></slot></button>
`;
注意::host 是关键,它代表宿主元素本身;var(--btn-bg) 能读取外部定义的 CSS 变量,这是实现主题定制的常用通路。
外部样式能影响 Shadow DOM 吗?只靠 CSS 变量和 :host() 伪类
不能直接选中内部节点,但有两条合法路径:
-
:host(.primary):外部给宿主加 class,:host()就能响应 - CSS 自定义属性(
var(--color)):在宿主上设置,内部<style>里读取,且支持继承
别试图用 !important 强行穿透,它在 shadow 边界前无效;也别依赖 /deep/ 或 ::ng-deep,这些早已被标准废弃,现代浏览器不支持。
使用 attachShadow() 前必须清空 innerHTML
如果组件模板是通过 innerHTML 初始化的,调用 attachShadow() 前没清空内容,老 DOM 节点会被自动移出(但不会销毁),导致视觉丢失或事件绑定失效。正确顺序是:
this.innerHTML = ''; // 先清空
this.attachShadow({ mode: 'open' }); // 再挂载 shadow
这个细节容易被忽略,尤其在从普通 div 改造成自定义元素时——页面突然空白,debug 时发现 shadowRoot 里啥都没有,大概率就是这里漏了。
Shadow DOM 的样式封装不是“一劳永逸”,而是需要主动设计穿透点(变量、host class)和明确边界(不依赖外部样式规则)。真正麻烦的从来不是 shadow 本身,而是如何让封装和可定制不互相打架。

















