容器查询是一项新的 CSS 功能,可让您编写以父元素的特征(例如宽度或高度)为目标的样式设置逻辑,以设置其子元素的样式。最近,发布了对 polyfill 的重大更新,与此同时,浏览器也进行了相应支持。
在这篇博文中,您将大致了解 polyfill 的工作原理、它克服的挑战,以及使用该 polyfill 为访问者提供出色用户体验的最佳做法。
深入了解
翻译
当浏览器中的 CSS 解析器遇到未知的 @ 规则(如全新的 @container
规则)时,只会将其丢弃,就好像它从未存在过。因此,polyfill 必须做的第一件事也是将 @container
查询转译为不会被舍弃的内容。
转译的第一步是将顶级 @container
规则转换为 @media 查询。这在很大程度上可以确保内容能够归为一组。例如,使用 CSSOM API 和查看 CSS 来源时。
@container (width > 300px) { /* content */ }
@media all { /* content */ }
在容器查询推出之前,CSS 无法让作者任意启用或停用规则组。如需对此行为执行 polyfill 操作,您还需要转换容器查询中的规则。每个 @container
都有自己的唯一 ID(例如 123
),该 ID 用于转换每个选择器,使其仅在元素具有包含此 ID 的 cq-XYZ
属性时应用。此属性将由 polyfill 在运行时设置。
@container (width > 300px) { .card { /* ... */ } }
@media all { .card:where([cq-XYZ~="123"]) { /* ... */ } }
请注意 :where(...)
伪类的使用。通常,添加额外的属性选择器会提高选择器的特异性。利用伪类,可以在保留原始特异性的同时应用额外的条件。要了解为什么这很重要,请思考以下示例:
@container (width > 300px) {
.card {
color: blue;
}
}
.card {
color: red;
}
在提供此 CSS 的情况下,具有 .card
类的元素应始终具有 color: red
,因为后面的规则会始终覆盖前面具有相同选择器和特异性的规则。因此,转译第一条规则并添加没有 :where(...)
的额外属性选择器会增加特异性,并导致错误应用 color: blue
。
不过,:where(...)
伪类是一项相当新的功能。对于不支持 polyfill 的浏览器,提供了一种安全简单的解决方法:您可以通过手动向 @container
规则中添加虚拟 :not(.container-query-polyfill)
选择器来有意提高规则的特异性:
@container (width > 300px) { .card { color: blue; } } .card { color: red; }
@container (width > 300px) { .card:not(.container-query-polyfill) { color: blue; } } .card { color: red; }
这样做有许多好处:
- 来源 CSS 中的选择器发生了变化,因此特异性的差异显而易见。这还可以充当文档,让您了解在不再需要支持解决方法或 polyfill 时,内容会受到什么影响。
- 规则的特异性将始终相同,因为 polyfill 不会对其进行更改。
在转译期间,polyfill 会将此虚拟替换为具有相同特异性的属性选择器。为避免任何意外,polyfill 使用这两个选择器:原始的源选择器用于确定元素是否应接收 polyfill 属性,而转译的选择器则用于样式设置。
伪元素
您可能会问自己的一个问题:如果 polyfill 在一个元素上设置一些 cq-XYZ
属性以包含唯一的容器 ID 123
,那么无法为无法设置属性的伪元素如何才能得到支持?
伪元素始终绑定到 DOM 中称为“原始元素”的实际元素。在转译期间,条件选择器会改为应用于此实际元素:
@container (width > 300px) { #foo::before { /* ... */ } }
@media all { #foo:where([cq-XYZ~="123"])::before { /* ... */ } }
条件选择器不会转换为 #foo::before:where([cq-XYZ~="123"])
(这会是无效的),而是被移到了原始元素(#foo
)的末尾。
然而,仅仅做到这些还不够。容器不能修改其中未包含的任何内容(容器也不在其内部),但请考虑一下,如果 #foo
本身就是被查询的容器元素,就会发生这种情况。系统会错误地更改 #foo[cq-XYZ]
属性,并错误地应用任何 #foo
规则。
为纠正此问题,polyfill 实际上使用了两个属性:一个属性只能由父项应用于元素,另一个属性可以应用于自身。后一个属性用于以伪元素为目标的选择器。
@container (width > 300px) { #foo, #foo::before { /* ... */ } }
@media all { #foo:where([cq-XYZ-A~="123"]), #foo:where([cq-XYZ-B~="123"])::before { /* ... */ } }
由于容器绝不会将第一个属性 (cq-XYZ-A
) 应用于自身,因此只有当其他父级容器满足容器条件并应用了第一个属性时,第一个选择器才会匹配。
容器相对单位
容器查询还附带了一些可在 CSS 中使用的新单元,例如 cqw
和 cqh
,分别对应最接近的相应父级容器的 1% 的宽度和高度。为了支持这些元素,系统会使用 CSS 自定义属性将单位转换为 calc(...)
表达式。polyfill 将通过容器元素上的内嵌样式设置这些属性的值。
.card { width: 10cqw; height: 10cqh; }
.card { width: calc(10 * --cq-XYZ-cqw); height: calc(10 * --cq-XYZ-cqh); }
此外,还有逻辑单元,例如,cqi
和 cqb
分别表示内嵌大小和块大小。这有点复杂,因为内嵌轴和块轴取决于使用单元的元素的 writing-mode
,而不是所查询的元素。为了支持这一点,polyfill 将内嵌样式应用于 writing-mode
与其父项不同的任何元素。
/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);
/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);
现在,可以像以前一样,将单位转换为相应的 CSS 自定义属性。
属性
容器查询还会添加一些新的 CSS 属性,例如 container-type
和 container-name
。由于 getComputedStyle(...)
等 API 无法用于未知或无效属性,因此这些属性在解析后也会转换为 CSS 自定义属性。如果无法解析某个属性(例如,由于它包含无效或未知的值),它只会留给浏览器处理。
.card { container-name: card-container; container-type: inline-size; }
.card { --cq-XYZ-container-name: card-container; --cq-XYZ-container-type: inline-size; }
每当发现这些属性时,它们都会进行转换,从而使 polyfill 能够与其他 CSS 功能(如 @supports
)完美配合。此功能是使用 polyfill 的最佳做法基础,具体如下所述。
@supports (container-type: inline-size) { /* ... */ }
@supports (--cq-XYZ-container-type: inline-size) { /* ... */ }
默认情况下,CSS 自定义属性是继承的,这意味着 .card
的任何子级都将采用 --cq-XYZ-container-name
和 --cq-XYZ-container-type
的值。这绝对不是原生媒体资源的运作方式。为解决此问题,polyfill 会在任何用户样式之前插入以下规则,以确保每个元素都接收初始值,除非被其他规则有意替换。
* {
--cq-XYZ-container-name: none;
--cq-XYZ-container-type: normal;
}
最佳做法
虽然预计大多数访问者会尽快运行带有内置容器查询支持的浏览器,但为其余访问者提供良好的体验仍然非常重要。
在初始加载期间,需要先执行许多操作,然后 polyfill 才能对页面进行布局:
- 需要加载并初始化该 polyfill。
- 需要解析和转译样式表。由于没有任何 API 可以访问外部样式表的原始来源,因此可能需要异步重新提取外部样式表,但理想情况下只是从浏览器缓存中提取。
如果 polyfill 未认真解决这些问题,可能会导致您的核心网页指标回归。
为了让您能更轻松地为访问者提供愉悦的体验,polyfill 旨在优先考虑 First Input Delay (FID) 和 Cumulative Layout Shift (CLS),这有可能牺牲 Largest Contentful Paint (LCP)。具体来说,polyfill 无法保证您的容器查询会在首次绘制之前得到评估。这意味着,为了提供最佳用户体验,您必须确保在 polyfill 加载并转译您的 CSS 之前,系统会隐藏任何大小或位置会因使用容器查询而受到影响的内容。实现此目的的方法之一是使用 @supports
规则:
@supports not (container-type: inline-size) {
#content {
visibility: hidden;
}
}
建议您将此操作与纯 CSS 加载动画结合使用,完全置于(隐藏)内容之上,以告知访问者正在发生的事情。您可以在此处查看此方法的完整演示。
建议使用此方法,原因如下:
- 纯 CSS 加载器可最大限度地降低使用新版浏览器的用户的开销,同时为使用旧版浏览器和网速较慢的用户提供轻量级反馈。
- 通过将加载器的绝对定位与
visibility: hidden
结合使用,您可以避免布局偏移。 - polyfill 加载后,此
@supports
条件将停止传递,并且系统将显示您的内容。 - 在内置支持容器查询的浏览器上,条件永远不会传递,因此网页会按预期在首次绘制时显示。
总结
如果您想在旧版浏览器上使用容器查询,不妨试试 polyfill。如果您遇到任何问题,可以随时提交问题。
我们迫不及待地想看到并体验您用它构建的精彩内容。
致谢
主打图片,作者:Dan Cristian PădureTITLE,制作者:Unsplash 网站。