
什么是影子DOM?
影子DOM 是 Web Components 标准套件中的一项关键技术。它允许你将一个隐藏的、独立的 DOM 树附加到一个常规的 DOM 元素上。
你可以把它想象成一个"DOM 中的 DOM",但它具有封装特性,外部的样式或者js无法影响到其内部:
-
主文档树
- 我们平时用
document.getElementById
等 API 直接操作的就是主 DOM 树
- 我们平时用
-
影子树
- 影子 DOM 内部的节点是独立于主文档的,它们不会被主文档的 JavaScript 或 CSS 所影响,反之亦然,影子dom里面的内容也无法影响到外面。
创建影子dom
可以通过Element.attachShadow()
方法来创建一个影子DOM
js
<script>
const dom = document.querySelector('.container')
const shadowRoot = dom.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = `
<style>
.shadow-box {
margin-top:50%;
width: 100%;
height: 50%;
background-color: rgba(0, 128, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
</style>
<div class="shadow-box">
<p>这是影子DOM内容</p>
</div>
`
</script>

可以看到在.container
下面多出了一个shadow-root
,这个就是我们的影子DOM
影子DOM的特性
这里我在外面设置一个样式

但是里面的shadow-box
的样式表完全没有继承到上面的样式,这就是影子DOM样式隔离性

我用js去获取.shadow-box
也是获取不到这个dom
的,外部的js也是没有办法直接获取到它的
获取影子DOM
如果想要获取到影子DOM,并且修改内部的内容或者样式,就需要通过shadowRoot
这个对象
注意:前提是创建影子dom的时候模式要设置为
open
js
const shadowRoot = dom.attachShadow({ mode: 'open' })

像上面的这个例子,我就通过有影子dom的父容器里面的shadowrRoot
对象访到了前面创建的影子dom
如果你创建影子dom的时候用的是closed
模式,那么外部的js就获取不到shadowrRoot
对象
就像下面这样

影子DOM的作用
影子DOM最重要的特性就是隔离性
-
🛡️ 内部样式不会"泄漏" :在Shadow DOM里写的
p { color: red; }
只会影响组件内部的段落,完全不用担心会影响到页面其他地方 -
🚫 外部样式无法"入侵" :全局样式、UI框架、甚至
!important
都无法穿透进来干扰你的组件(除了少数继承属性) -
🎯 告别命名冲突:再也不需要BEM、CSS Modules那些复杂的命名约定,直接用最简单的选择器就行
如果设置了closed
模式,那么:
- 🔒 外部JavaScript无法直接通过
document.querySelector
窥探或操作你的组件内部 - 🛡️ 第三方脚本再也无法意外破坏你的精心布局
- 💪 组件真正实现了"高内聚",内部细节被完美隐藏
应用场景
得益于影子DOM的高度隔离性,它非常适合在做组件库或者微前端架构的时候使用,这样可以保证你的样式隔离,不会被奇奇怪怪的全局样式污染.
举个🌰
js
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `
<style>
button {
padding: 12px 24px;
border-radius: 8px;
border: none;
background: #007bff;
color: white;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
<button><slot></slot></button>
`;
}
}
customElements.define('my-button', MyButton);
像这样在封装组件的时候,可以像这样通过影子dom
去创建.
或者在微前端中用影子DOM
去包裹子应用
js
// 主应用加载微前端
function loadMicroApp(container, appUrl) {
const shadowContainer = container.attachShadow({mode: 'open'});
// 加载微前端内容到 shadowContainer
}
这样就可以避免样式冲突引发一些奇奇怪怪的问题
还有一种就是接入第三方组件的时候,也可以用这个影子dom去包裹,让他的样式不会影响到外面
修改影子DOM样式的几种方式
当然最重要的时候,我们要如何修改影子DOM的样式,有以下几种方式
CSS变量
由于shadow dom
是可以读取到我们外部设置的css 变量
,所以可以使用和修改 变量的方式,来改写样式,就像下面这样
css
/* 在主文档中定义主题变量 */
:root {
--primary-color: #007bff;
--component-bg: #ffffff;
--component-padding: 1rem;
}
js
const dom = document.querySelector('.container')
const shadowRoot = dom.attachShadow({ mode: 'closed' })
shadowRoot.innerHTML = `
<style>
.shadow-box {
margin-top:50%;
width: 100%;
height: 50%;
background-color: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
</style>
<div class="shadow-box">
<p>这是影子DOM内容</p>
</div>
`

通过上面的截图可以看到, 影子DOM内部可以访问到外部的css变量
::part() 伪元素
::part
CSS 伪元素表示在阴影树中任何匹配 part
属性的元素。
只要给影子DOM
设置part
属性,就可以通过这个伪元素去修改它的样式
css
.container::part(shadow-box) {
background: red;
}
js
shadowRoot.innerHTML = `
<style>
.shadow-box {
margin-top:50%;
width: 100%;
height: 50%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
</style>
<div class="shadow-box" part="shadow-box">
<p>这是影子DOM内容</p>
</div>
`
效果如下:

通过js处理
这种方式不是很推荐,因为破坏了Shadow DOM的设计初衷,导致组件脆弱难维护
前面说过如果创建影子dom的模式是open
,那么我们就可以通过shadowRoot
去获取里面的DOM,能获取到里面的dom
,修改样式就很容易了
就像下面这样

通过内部修改
这部分我觉得作用不是那么的大,了解就好.
:host
用于设置宿主元素(即组件本身)的默认样式或状态样式。
html
<!-- 在 Shadow DOM 的 <style> 标签内 -->
<style>
/* 设置组件自身的默认样式 */
:host {
display: block; /* 最重要!自定义元素默认是inline */
margin: 0.5rem;
padding: 1rem;
border: 1px solid #ccc;
}
/* 当组件有 'active' 属性时 */
:host([active]) {
border-color: blue;
background-color: aliceblue;
}
/* 当组件有 'disabled' 属性时 */
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
</style>
场景 :定义组件容器的基础样式,或根据属性(如 disabled
, size="large"
)改变整体外观。
:host-context()
用于根据组件所在的外部祖先元素来应用样式。
html
<style>
/* 当我的某个祖先元素有 .dark-theme 类时 */
:host-context(.dark-theme) .card {
background-color: #333;
color: white;
}
/* 当我在一个侧边栏内时 */
:host-context(app-sidebar) {
margin: 0;
border-left: none;
}
</style>
场景:让组件自动适配外部主题(如深色模式)或特定布局容器。
::slotted()
用于修饰通过 <slot>
插槽投射进来的用户提供的 Light DOM 内容 。注意,只能改变它的字体、颜色等样式,不能改变布局(如 display
, margin
)。
html
<style>
/* 为所有插槽元素添加基础样式 */
::slotted(*) {
margin-bottom: 0.5rem;
}
/* 特别修饰插槽中的 h3 标签 */
::slotted(h3) {
color: var(--primary-color, blue);
border-bottom: 2px solid currentColor;
}
/* 修饰带有 .highlight 类的插槽元素 */
::slotted(.highlight) {
background-color: yellow;
padding: 0.25rem;
}
</style>
场景:对用户传入的内容进行基础的样式装饰,保持与组件风格的统一。
总结
总而言之,Shadow DOM 绝非一个遥远而晦涩的概念,而是现代前端开发中解决**"隔离"与"封装"**两大核心痛点的利器。它通过创建独立的 DOM 树,带来了真正的样式和 DOM 隔离,让你能够:
- 自信地编写组件:无需再担心选择器命名冲突,可以使用最简单直观的CSS。
- 构建可靠的应用:无论是微前端架构还是引入第三方库,Shadow DOM 都是一道可靠的屏障,确保各个部分互不干扰。
- 提供灵活的API :通过 CSS 变量和
::part()
等方式,对外提供可控、安全的样式定制接口,而不是暴露脆弱的内部实现。
掌握 Shadow DOM,意味着你掌握了构建高内聚、低耦合、易于维护的现代化 Web 应用的关键技能。它将帮助你从被动地解决样式冲突,转向主动地设计封装良好的组件体系。