容器查询是一项 CSS 新功能,可让您编写样式逻辑,以定位到父元素的特性(例如其宽度或高度)来为其子元素设置样式。我们最近发布了polyfill 的重大更新,与浏览器中推出的支持功能同步发布。
在本文中,您将了解 polyfill 的运作方式、它克服的挑战,以及在使用它为访问者提供出色用户体验时的最佳实践。
深入了解
转译
当浏览器中的 CSS 解析器遇到未知的 at 规则(例如全新的 @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
的末尾,而不是转换为 #foo::before:where([cq-XYZ~="123"])
(这将无效)。
不过,这还不是全部。容器不得修改其自身不包含的任何内容(容器也不能位于自身内部),但请注意,如果 #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 能够与 @supports
等其他 CSS 功能完美配合。此功能是使用 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 未妥善解决这些问题,可能会导致您的核心 Web Vitals 指标出现回归现象。
为了让您更轻松地为访问者提供愉快的体验,该 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。如果您遇到任何问题,欢迎随时提交问题。
我们迫不及待地想要看到和体验您使用它构建的令人惊叹的应用。