12k
All articles

使用 Shadow DOM 和 CSS 为 Web Components 设置样式

使用 Shadow DOM 为 Web Components 编写样式:通过 :host、::slotted()、CSS 变量、::part() 和 adoptedStyleSheets 控制组件 CSS。

OpenReplay Team
OpenReplay Team
使用 Shadow DOM 和 CSS 为 Web Components 设置样式

如果你曾经将 Web Component 放入页面,然后疑惑为什么全局 CSS 在其内部不起作用,那么你就遇到了 shadow 边界。这并不是 bug——而正是其设计初衷。Shadow DOM 为自定义元素提供了独立的作用域 DOM 树,这意味着样式默认不会渗入或泄漏出来。但这并不意味着无法设置样式,而是意味着样式设置是 有意为之 的。

本文涵盖了 Shadow DOM 样式设置的主要机制:内部样式、:host::slotted()、CSS 自定义属性、::part() 以及 adoptedStyleSheets

核心要点

  • Shadow DOM 按设计对样式进行作用域隔离,因此全局 CSS 无法到达组件内部,内部样式也无法泄漏出来。
  • Shadow 样式作为默认值,在 CSS 级联中位于页面作者样式之下,类似于浏览器的用户代理样式表。
  • 使用 :host:host() 为组件元素设置样式,使用 ::slotted() 为投影的 light DOM 子元素(仅限直接后代)设置样式。
  • CSS 自定义属性和 ::part() 共同构成了一个有意设计的公共样式 API:变量用于主题令牌,parts 用于结构化样式。
  • adoptedStyleSheets 允许多个组件共享单个已解析的 CSSStyleSheet,在大规模场景下提升性能。

为什么 Shadow DOM 会改变 CSS 的行为方式

当你在元素上调用 attachShadow() 时,你会创建一个独立的 DOM 树,附加到该元素(即 shadow host)。页面样式表中的选择器无法到达该树内部,shadow 树内部的选择器也无法触及外部。

有一个重要的细节值得理解:shadow DOM 样式在 CSS 级联中位于你的作者样式 之下。这意味着像 * { color: red } 这样的全局规则会覆盖 shadow 内部的 :host { color: green } 规则——即使全局规则在源代码顺序中出现得更早。可以将 shadow 样式看作组件的默认样式,类似于浏览器的用户代理样式表为 <button><input> 提供默认值的方式。

使用 :host 为 Shadow Host 设置样式

在 shadow 树内部,:host 选择附加 shadow 的那个元素:

:host {
  display: block;
  font-family: sans-serif;
}

你也可以使用 :host(selector) 根据 host 上的属性或类有条件地应用样式:

:host([disabled]) {
  opacity: 0.5;
  pointer-events: none;
}

由于 host 元素位于 light DOM 中,文档级样式会覆盖 :host 规则。如果你希望组件的默认值胜出,可以在 shadow 内部使用 !important——这是真正适合使用它的少数场景之一。

使用 ::slotted() 为插槽内容设置样式

插槽(slot)允许你将 light DOM 内容投影到 shadow 树中。::slotted() 伪元素允许你从 shadow 内部为这些投影元素设置样式:

::slotted(p) {
  margin: 0;
  color: #333;
}

重要限制: ::slotted() 只匹配直接分配给插槽的元素——不包括其后代。像 ::slotted(p span) 这样的选择器不会生效,并且 ::slotted() 只接受复合选择器(不能使用后代组合符)。要进行更深层的样式设置,请依赖 CSS 继承,或让 light DOM 自己的样式来处理。

CSS 自定义属性可以跨越 Shadow 边界

CSS 变量(自定义属性)可以自由穿透 shadow 边界。这使它们成为 Web Components 主题化 最灵活的工具:

/* In the shadow tree */
:host {
  background: var(--card-bg, white);
  color: var(--card-color, black);
}
/* In your page stylesheet */
my-card {
  --card-bg: #1a1a2e;
  --card-color: #eee;
}

组件定义钩子,使用者设置值。始终包含回退值,这样组件在没有任何外部配置的情况下也能工作。

使用 CSS Shadow Parts (::part()) 暴露样式钩子

CSS Shadow Parts 是从外部为 Web Component 内部元素设置样式的现代解决方案。在组件内部,你用 part 属性标记元素:

<button part="trigger">Open</button>

在组件外部,使用者可以使用 ::part() 直接定位该 part:

my-dialog::part(trigger) {
  background: royalblue;
  border-radius: 4px;
}

这是一个有意设计的样式 API——组件作者决定暴露什么。对于结构化样式(布局、边框、背景),它比 CSS 变量更强大,同时保持内部实现细节的私密性。

使用 adoptedStyleSheets 高效共享样式

adoptedStyleSheets 允许你将 CSSStyleSheet 对象直接附加到 shadow root 上。它在现代浏览器中得到广泛支持,非常适合需要在多个实例间共享一个已解析样式表的组件库:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.adoptedStyleSheets = [sheet];
  }
}

customElements.define('my-component', MyComponent);

无论有多少组件使用,浏览器都只解析一次样式表——在大规模场景下这是一个显著的性能优势。你可以通过修改 adoptedStyleSheets 数组来添加或移除样式表,对共享 CSSStyleSheet 的更改会应用到所有采用它的地方。

选择合适的方案

目标工具
为组件的 host 元素设置样式:host / :host()
为投影的 light DOM 内容设置样式::slotted()
向使用者暴露主题令牌CSS 自定义属性
向使用者暴露结构化样式::part()
在多个组件间共享样式adoptedStyleSheets

总结

Shadow DOM 样式设置并不是要把 CSS 锁在外面——而是要让封装变得明确。使用内部样式和 :host 设置默认值,使用 CSS 变量和 ::part() 作为公共样式 API,在大规模性能至关重要时使用 adoptedStyleSheets。一旦建立了这种心智模型,为 Web Components 设置样式就会变得简单明了。

常见问题

为什么我的全局 CSS 不影响 Web Component 的内容?

因为该组件使用了 Shadow DOM,它创建了一个与主文档隔离的作用域 DOM 树。页面样式表无法选择 shadow 树内的元素。要为内部元素设置样式,组件作者必须暴露钩子,例如 CSS 自定义属性,或通过 part 属性暴露 CSS Shadow Parts,然后你可以从外部使用 ::part() 来定位它们。

什么时候应该使用 ::part() 而不是 CSS 自定义属性?

对于颜色、间距和字体等可以通过继承自然流动的设计令牌,使用自定义属性。当使用者需要对特定的内部元素进行结构化控制时(例如覆盖按钮或标题的边框、背景或布局),使用 ::part()。Parts 提供更细粒度的访问,而变量则更简单、作用域更广。

为什么 ::slotted(p span) 匹配不到任何东西?

::slotted() 伪元素只匹配直接分配给插槽的顶层节点,不包括其后代。它也只接受复合选择器,因此后代组合符是无效的。要为插槽元素的子元素设置样式,可以依赖来自插槽元素的 CSS 继承,或让使用者自己的 light DOM 样式表直接处理这些后代。

adoptedStyleSheets 在生产环境中可以安全使用吗?

可以。它在所有现代常青浏览器中都得到了支持,是在多个 shadow root 之间共享样式而无需重新解析的推荐方式。一个已解析的 CSSStyleSheet 可以附加到多个 shadow root 上,从而减少内存使用并改善启动时间。共享样式表还可以在多个组件间高效地更新和重用。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.