容器查询是一项新的 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 的浏览器,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ădure 上传,Unsplash 用户。