Shadow DOM 是一个样式边界:页面规则不会进入,组件规则也不会泄露,但继承属性(包括自定义属性)除外。通过以下渠道故意跨越。
:host、:host()、:host-context()
/* The component's own host element, styled from inside its shadow tree. */
:host { display: block; }
/* Only when the host matches a selector (e.g. a reflected state attribute). */
:host([disabled]) { opacity: 0.5; pointer-events: none; }
/* Based on an ancestor in the Light DOM; useful for theming. */
:host-context([theme="dark"]) { background: #111; color: #eee; }
避免使用 :host-context()。它会超出边界来测试祖先元素,但 Firefox 和 Safari 已拒绝发布它,CSSWG 也倾向于不再进一步开发它。因此,请将其视为仅限 Chromium 的死胡同,而不是具有前瞻性的选项。通过继承的 CSS 自定义属性(如下所示)设置主题:使用者在祖先元素上设置属性,组件跨边界读取该属性,这在任何地方都有效,并使双方保持解耦。
沿用的属性和自定义属性跨越边界
继承的 CSS 属性(color、font-family、line-height 和 CSS 自定义属性)会穿过 shadow 边界。这使得自定义属性成为主要的样式设置 API:通过合理的后备值公开它们。
:host {
/* Consumers override --card-bg from outside; the fallback keeps it usable. */
background: var(--card-bg, white);
color: var(--card-fg, black);
}
在 :host 上定义的自定义属性只能被专门针对宿主元素的规则替换。如需让其继承并可从树中的任何位置替换,请不要在 :host 上设置公开属性,而是依赖于每个 var() 中的后备属性;不要在 :host 上植入公开属性。
为了避免在每个 var() 中重复声明后备值,请在 :host 上定义一个私有“影子”属性,用于读取公共属性,并在内部引用该私有属性:
:host {
--_card-bg: var(--card-bg, white);
--_card-fg: var(--card-fg, black);
}
.card { background: var(--_card-bg); color: var(--_card-fg); }
使用者仍会从外部替换 --card-bg;前导下划线表示别名是内部别名,并且后备别名仅存在于一个位置。
::slotted():设置投影 Light DOM 的样式
::slotted(selector) 定位到分配给槽的顶级节点。它无法触及后代,并且会输给任何 Light DOM 规则(使用者拥有自己的内容),因此请将其用于轻微修饰,而不是控制。::slotted(...) !important 可以覆盖消费者规则,但这样做会覆盖消费者有权拥有的样式,因此请将其保留用于结构修复,而不是美学覆盖。
/* Matches a slotted <p>, but NOT a <p> nested inside a slotted <div>. */
::slotted(p) { margin: 0; }
::part() 和 exportparts:公开内部组件以支持主题设置
使用 part="name" 标记内部元素,让使用者可以使用任何属性设置其样式,而无需了解您的内部结构。
<!-- inside the shadow template -->
<button part="submit">Submit</button>
/* consumer stylesheet */
my-card::part(submit) { background: navy; color: white; }
公开一些高级部分(按钮、输入、标签),而不是每个 <div>;广泛的部分表面会成为脆弱的 API。如需通过您自己的组件向上重新公开嵌套组件的各个部分,请使用 exportparts 转发这些部分:
<inner-widget exportparts="submit: card-submit"></inner-widget>
请谨慎选择这两种渠道:自定义属性适用于受限且符合设计系统安全要求的一组值;部分适用于对特定元素进行不受限制的样式设置。
将样式转发到封装的原生元素
当自定义元素封装原生控件(<my-button> 封装实际 <button>)时,通常有三件事会让人感到困惑:
- 表单控件不会继承排版。
color和自定义属性会跨越边界,但<button>、<input>和<select>会从 UA 样式表中获取font及其相关属性,而这些属性不会继承。因此,封装的控件会以平台字体(而非页面字体)呈现,除非您在 shadow tree 中为其设置font: inherit(通常还有color: inherit)。这是最常见的失误。 :host样式无法到达内部元素。在:host上设置background/border/padding会设置宿主框的样式,而不是其内部控件的样式。谨慎转发:将控件公开为::part以实现开放式样式,或通过自定义属性传递一组固定值 -:host规则不会级联到封装的元素中。display: contents+all: inherit是一种真正的权衡,而不是捷径。目前,在不让使用者学习每个组件部分或自定义属性的情况下,将任意样式传递给封装的原生元素的唯一通用方法是:使用display: contents折叠宿主,并使用all: inherit重置内部控件。移除宿主自己的框通常是该模式的要点(组合可弥补缺失的内置继承)。两个实际注意事项:all: inherit会清除 UA 默认值,因此请限定到特定属性,或在需要恢复默认值时使用revert;以及具体阻碍因素:getBoundingClientRect()和getClientRects()会针对display: contents元素返回零,因此针对宿主的锚点定位会失效。如果这些权衡不合适,请使用::part或自定义属性。
:state():设置组件自身状态的样式
自定义元素可以通过 ElementInternals 经由 this.#internals.states.add('busy') 和 .delete('busy') 公开内部布尔状态(如需了解生命周期用法,请参阅自定义元素),这些状态会成为跨边界的样式设置渠道,类似于 ::part 和自定义属性,但适用于状态,而非结构或值。在 shadow tree 中使用 :host(:state(name)) 匹配它们,在消费者的样式表中使用 tag-name:state(name) 匹配它们:
/* Inside the component's shadow tree. */
:host(:state(busy)) button { opacity: 0.6; pointer-events: none; }
/* From the consumer's stylesheet — a read-only public hook. */
favorite-button:state(favorited) { filter: drop-shadow(0 0 4px crimson); }
与反射属性不同,自定义状态对外部是只读的:使用者可以设置其样式,但无法设置或伪造它,因此组件仍然是唯一的写入者。只要状态由组件拥有且只需设置样式,就应优先使用它,而不是 data-* 属性。
组件内的 @layer 和容器查询
- 级联层 (
@layer) 在 shadow root 内运行,并限定在 shadow root 内;shadow tree 中的层名称与页面中同名的层无关。使用它们来对您自己的组件样式(例如@layer base, theme;)进行排序,以便通过自定义属性和::part进行的使用方替换仍能按预期生效。 - 容器查询是响应式组件的理想工具:组件应响应其所在的空间,而不是视口。将宿主或封装容器声明为容器(为
:host提供布局框,例如display: block,因为自定义元素默认为display: inline,并且大小包含不适用于inline或contents元素)并对其进行查询,以便同一组件在边栏或全宽区域中自适应。
:host {
display: block; /* Required: size containment does not apply to default display: inline */
container-type: inline-size;
}
@container (min-width: 30rem) {
.card { grid-template-columns: 1fr 2fr; }
}
后备策略
::part、级联层和容器查询已广泛可用;主题主要通过继承的自定义属性实现,即使在这些功能不可用的情况下,自定义属性也能正常工作。如上所述,请勿依赖 :host-context(),它实际上仅适用于 Chromium,并非前瞻性选项。