作者定义的 CSS 名称和 shadow DOM:规范和实践

作者定义的 CSS 名称和 Shadow DOM 应协同发挥作用。不过,浏览器与规范不一致,有时彼此之间也不一致,而且每个 CSS 名称的不一致性略有不同。

本文档记录了作者定义的 CSS 名称在阴影作用域中的行为方式的当前状态,希望能作为指南,帮助我们在不久的将来改进互操作性。

什么是作者定义的 CSS 名称?

作者定义的 CSS 名称是一种相对较旧的 CSS 语法机制,最初是为 @keyframes 规则引入的,该规则将 <keyframe-name> 定义为自定义标识符或字符串。此概念的用途是在样式表的一部分中声明某项内容,并在另一部分中引用该内容。

/* "fade-in" is a CSS name, representing a set of keyframes */
@keyframes fade-in {
  from { opacity: 0 };
  to { opacity: 1 }
}

.card {
  /* "fade-in" is a reference to the above keyframes */
  animation-name: fade-in;
}

使用 CSS 名称的其他 CSS 功能包括字体、属性声明、容器查询,以及最近推出的视图转换、锚点定位和滚动驱动型动画。以下非详尽表格包含 Chrome 会检查状态的名称。

功能 名称声明 名称引用
关键帧 @keyframes animation-name
字体 @font-face { }
@font-palette-values
font-family
font-palette
属性声明 @property
任何未注册的自定义属性声明
var()
视图过渡 view-transition-name
view-transition-class
::view-transition-* 伪元素
锚点定位 anchor-name position-anchor
滚动条驱动的动画 view-timeline-name
scroll-timeline-name
animation-timeline
列表样式 @counter-style list-style
计数器 counter-reset
counter-set
counter-increment
容器查询 container-name @container
网页 page @page

如表格所示,CSS 名称通常具有相应的 CSS 引用。例如,animation-name 是对 @keyframes 名称的引用。CSS 名称不同于 DOM 中定义的名称(例如属性和标记名称),因为它们是在样式表上下文中声明和引用的。

名称与 shadow DOM 的关系

虽然 CSS 名称旨在在文档或样式表的不同部分之间建立关系,但 Shadow DOM 的用途则恰恰相反。它封装了关系,以免这些关系在应该具有自己命名空间的 Web 组件之间泄露。

通过将 CSS 名称与阴影 DOM 结合使用,Web 组件组合体验应该足够灵活,但又足够受限,以确保稳定性。

这在理论上是可行的。在实践中,浏览器在 CSS 名称与 Shadow DOM 的互操作方式方面存在不一致性,无论是在同一浏览器中的功能之间、不同浏览器之间,还是在功能与规范之间。

名称和 Shadow DOM 应如何协同运作

为了了解问题所在,不妨了解 CSS 的这些部分在理论上应如何协同工作。

一般规则

CSS 范围级别 1 规范中定义了 CSS 名称在各个阴影树中的行为方式的一般规则。总而言之:CSS 名称在其定义的范围内是全局的,这意味着可以从子孙阴影树访问它,但不能从同级或父级阴影树访问它。请注意,这与 Web 平台中的名称(例如元素 ID)不同,后者封装在同一树范围内。

规则例外情况:@property

与其他 CSS 名称不同,CSS 属性不会封装在 Shadow DOM 中。而是在不同的阴影树之间传递参数的常用方法。这使得 @property 描述符具有特殊性:它的行为应类似于文档全局类型声明,用于定义特定命名属性的行为方式。由于属性必须在阴影树中保持一致,因此属性声明不匹配会导致意外结果,因此指定了 @property 声明,以便根据文档顺序进行扁平化和解析。

规则应如何与 ::part 搭配使用

阴影部分会将阴影树中的元素公开给其父树。这样一来,父树就可以访问该元素,还可以使用 ::part 元素为其设置样式。

由于 ::part 允许两个树级范围为同一元素设置样式,因此指定了以下级联顺序:

  1. 首先,检查阴影上下文中的样式。这是组件的“默认”样式。
  2. 然后,应用 ::part 中定义的外部样式。这是该组件的“自定义”样式。
  3. 然后,应用与 !important 一起定义的任何内部样式。 这样,自定义元素就可以声明某个部分的某个属性无法通过 ::part 进行自定义。

这意味着,您无法从 ::part 引用 shadow DOM 中的名称,因为 ::part 是宿主级范围的样式,而不是阴影级范围的样式。例如:

// inside the shadow DOM:
@keyframes fade-in {
  from { opacity: 0}
}

// This shouldn't work!
// The host style shouldn't know the name "fade-in"
::part(slider) {
  animation-name: fade-in;  
}

该规则应如何与内嵌样式搭配使用

::part 不同,具有 style 属性的内嵌样式或使用脚本以编程方式设置样式的内嵌样式,其作用域仅限于元素的作用域。这是因为,若要向元素应用样式,您需要访问元素手柄,进而访问影子根本身。

CSS 名称和 Shadow DOM 在现实中的协同运作方式

虽然上述规则定义明确且一致,但当前的实现并不总是反映这一点。实际上,@property 在不同浏览器中的运作方式与规范不同,但方式是一致的,并且大多数其他功能都存在未解决的 bug(其中一些尚未发布,因此有时间进行修复)。

为了测试和演示这些功能在实践中的运作方式,我们创建了以下页面:https://css-names-in-the-shadow.glitch.me/。此页面包含多个 iframe,每个 iframe 都侧重于一项功能,并测试以下六种场景:

  • 对外部名称的外部引用:不涉及 shadow DOM,应该可以正常运行。
  • 对内部名称的外部引用:这不起作用,因为这意味着在阴影上下文中定义的名称已泄露。
  • 对外部名称的内部引用:这应该可以正常运行,因为树级范围的名称会被阴影根继承。
  • 对内部名称的内部引用:由于引用的名称都位于同一作用域中,因此应该可以正常运行。
  • ::part 引用外部名称:由于 ::part 和名称都在同一作用域中声明,因此此方法应该可行。
  • ::part 引用内部名称:这不起作用,因为外部作用域不应了解 shadow DOM 中声明的名称。

@keyframes

正如规范中所定义,只要 @keyframes at-rule 位于祖先作用域中,您应该就可以从阴影根中引用关键帧名称。实际上,没有任何浏览器会实现此行为,并且关键帧定义只能在其定义的范围内引用。请参阅问题 10540

@property

如规范中所定义,任何 @property 声明都将展平到文档作用域。不过,目前在所有浏览器中,您只能在文档作用域中声明 @property,阴影根中的 @property 声明会被忽略。
请参阅问题 10541

浏览器专用 bug

其他功能在不同浏览器中的行为不一致:

  • @font-face 会在 Safari 中展平为根作用域。
  • Chromium 不允许在阴影根中继承 anchor-name 规则
  • scroll-timeline-nameview-timeline-name::part 上(也适用于 Chromium)的范围未正确设置。
  • 没有任何浏览器允许在阴影根中声明 @font-palette-values
  • view-transition-class 可以在阴影根内定义(转场效果本身位于阴影根之外)。
  • Firefox 允许 ::part 访问内部阴影名称(容器查询、关键帧)。
  • Firefox 和 Safari 不遵循阴影根中的 @counter-style

请注意,counter-resetcounter-setcounter-increment 的规则略有不同,因为它们是隐式名称,而声明 CSS 属性有一套经过充分测试的既定规则。

总结

坏消息是,在检查与 CSS 名称和 shadow DOM 相关的当前互操作性状态的快照时,体验不一致且存在 bug。我们在此处检查的所有功能在各浏览器中都无法按照规范一致地运行。好消息是,要想使体验保持一致,需要解决的 bug 和规范问题数量有限。让我们来解决这个问题! 与此同时,如果您在处理本文中所述的不一致性时遇到问题,希望本概览能对您有所帮助。