作者定义的 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 名称和 shadow DOM 结合在一起,编写网页组件的体验应富有表现力,既能保持灵活性,又能保持稳定。

从理论上来说,这是很好的做法。在实践中,浏览器在 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

特定于浏览器的错误

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

  • @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 和规范问题。让我们来解决这个问题! 与此同时,如果您在处理本文中所述的各种不一致问题时遇到困难,希望本概览能对您有所帮助。