CSS 容器查询:用例和迁移策略

已发表: 2022-03-10
快速总结↬ CSS 容器查询使媒体查询更接近目标元素本身,并使它们能够适应几乎任何给定的容器或布局。 在本文中,我们将介绍 CSS 容器查询的基础知识,以及如何将它们与渐进式增强或 polyfill 一起使用。

当我们为 UI 元素编写媒体查询时,我们总是根据屏幕尺寸描述该元素的样式。 当目标元素媒体查询的响应性应仅取决于视口大小时,此方法效果很好。 让我们看一下下面的响应式页面布局示例。

响应式页面布局示例。左图显示了 1280 像素视口宽度的桌面布局,右图显示了 568 像素视口宽度的移动版布局。
响应式页面布局示例。 左图显示了 1280 像素视口宽度的桌面布局,右图显示了 568 像素视口宽度的移动版布局。 (大预览)

但是,响应式 Web 设计 (RWD) 并不局限于页面布局——单个 UI 组件通常具有可以根据视口尺寸改变其样式的媒体查询。

响应式产品卡片组件示例。左图显示了 720px 视口宽度的组件,右图显示了 568px 视口宽度的组件布局。
响应式产品卡片组件示例。 左图显示了 720px 视口宽度的组件,右图显示了 568px 视口宽度的组件布局。 (大预览)

您可能已经注意到前面陈述的一个问题——单个 UI 组件布局通常并不完全依赖于视口尺寸。 虽然页面布局是与视口尺寸密切相关的元素,并且是 HTML 中最顶层的元素之一,但 UI 组件可以在不同的上下文和容器中使用。 如果你仔细想想,视口只是一个容器,UI 组件可以嵌套在其他容器中,其样式会影响组件的尺寸和布局。

顶部 3 列网格和底部列表中具有相同产品卡 UI 组件的页面布局示例。
顶部 3 列网格和底部列表中具有相同产品卡 UI 组件的页面布局示例。 (大预览)

即使在顶部和底部都使用相同的产品卡片组件,组件样式不仅取决于视口尺寸,还取决于上下文和放置它的容器 CSS 属性(如示例中的网格)。

当然,我们可以构建我们的 CSS,以便我们支持不同上下文和容器的样式变化,以手动解决布局问题。 在最坏的情况下,这种变体将添加样式覆盖,这将导致代码重复和特异性问题。

 .product-card { /* Default card style */ } .product-card--narrow { /* Style variation for narrow viewport and containers */ } @media screen and (min-width: 569px) { .product-card--wide { /* Style variation for wider viewport and containers */ } }

但是,这更多是针对媒体查询限制的解决方法,而不是适当的解决方案。 在为 UI 元素编写媒体查询时,当目标元素具有布局不会中断的最小尺寸时,我们试图为断点找到一个“神奇”的视口值。 简而言之,我们将“神奇的”视口尺寸值链接到元素尺寸值。 该值通常与视口尺寸不同,并且在内部容器尺寸或布局更改时容易出现错误。

媒体查询如何无法可靠地链接到元素维度的示例。各种 CSS 属性会影响容器内的元素尺寸。在此示例中,两个图像之间的容器填充不同。
媒体查询如何无法可靠地链接到元素维度的示例。 各种 CSS 属性会影响容器内的元素尺寸。 在此示例中,两个图像之间的容器填充不同。 (大预览)

下面的示例展示了这个确切的问题——即使响应式产品卡片元素已经实现并且在标准用例中看起来不错,但如果将其移动到具有影响元素尺寸的 CSS 属性的不同容器中,它看起来会损坏。 每个额外的用例都需要添加额外的 CSS 代码,这可能导致重复代码、代码膨胀和难以维护的代码。

请参阅 Adrian Bece 的 Pen [产品卡:各种容器](https://codepen.io/smashingmag/pen/eYvWVxz)。

请参阅钢笔产品卡片:Adrian Bece 的各种容器。

这是容器查询试图解决的问题之一。 容器查询通过依赖于目标元素尺寸的查询扩展了现有的媒体查询功能。 使用这种方法有三个主要好处:

  • 容器查询样式的应用取决于目标元素本身的尺寸。 UI 组件将能够适应任何给定的上下文或容器。
  • 开发人员不需要在特定容器或特定上下文中寻找将视口媒体查询链接到 UI 组件的目标维度的“幻数”视口维度值。
  • 无需为不同的上下文和用例添加额外的 CSS 类或媒体查询。
“理想的响应式网站是一个灵活的模块化组件系统,可以重新调整用途以在多种情况下提供服务。”

——“容器查询:再次违反”,Mat Marquis

在深入研究容器查询之前,我们需要检查浏览器支持并了解如何在浏览器中启用实验性功能。

浏览器支持

容器查询是一项实验性功能,在撰写本文时目前在 Chrome Canary 版本中可用。 如果您想继续并运行本文中的 CodePen 示例,您需要在以下设置 URL 中启用容器查询。

 chrome://flags/#enable-container-queries
容器查询
(大预览)

如果您使用的浏览器不支持容器查询,将在 CodePen 演示旁边提供展示预期工作示例的图像。

跳跃后更多! 继续往下看↓

使用容器查询

容器查询不像常规媒体查询那么简单。 我们必须在我们的 UI 元素中添加一行额外的 CSS 代码才能使容器查询正常工作,但这是有原因的,我们将在接下来介绍这一点。

收容属性

CSS contain属性已添加到大多数现代浏览器中,并且在撰写本文时已获得 75% 的浏览器支持。 contain属性主要用于性能优化,通过向浏览器提示页面的哪些部分(子树)可以被视为独立的并且不会影响对树中其他元素的更改。 这样,如果单个元素发生更改,浏览器将仅重新呈现该部分(子树)而不是整个页面。 使用 contains 属性值,我们可以指定要使用的contain类型—— layoutsizepaint

contain很多关于 contains 属性的精彩文章更详细地概述了可用选项和用例,因此我将只关注与容器查询相关的属性。

用于优化的 CSS contentment 属性与容器查询有什么关系? 为了使容器查询起作用,浏览器需要知道元素的子布局中是否发生更改,它应该只重新渲染该组件。 当组件被渲染或组件的尺寸发生变化时,浏览器将知道将容器查询中的代码应用到匹配的组件。

我们将使用layout​ style​作为contain​ ,但我们还需要一个附加值来向浏览器发出关于发生更改的轴的信号。

  • inline-size
    内联轴上的遏制。 预计这个值会有更多的用例,所以它首先被实施。
  • block-size
    块轴上的遏制。 它仍在开发中,目前不可用。

contain属性的一个小缺点是我们的布局元素需要是contain元素的子元素,这意味着我们正在添加一个额外的嵌套级别。

 <section> <article class="card"> <div class="card__wrapper"> <!-- Card content --> </div> </article> </section>
 .card { contain: layout inline-size style; } .card__wrapper { display: grid; grid-gap: 1.5em; grid-template-rows: auto auto; /* ... */ }

请注意,我们如何不将此值添加到更远的类似父级的section ,并使容器尽可能靠近受影响的元素。

“绩效是避免工作并使您所做的任何工作尽可能高效的艺术。 在许多情况下,它是关于使用浏览器,而不是反对它。”

——“渲染性能”,保罗·刘易斯

这就是为什么我们应该正确地向浏览器发出有关更改的信号。 使用 contains 属性包装远距离父元素contain会适得其反,并对页面性能产生负面影响。 在滥用contain属性的最坏情况下,布局甚至可能中断,浏览器将无法正确呈现它。

容器查询

在将contain属性添加到卡片元素包装器后,我们可以编写容器查询。 我们已经为具有card类的元素添加了一个contain属性,因此现在我们可以在容器查询中包含它的任何子元素。

就像常规媒体查询一样,我们需要使用min-widthmax-width属性定义一个查询,并将所有选择器嵌套在块内。 但是,我们将使用@container关键字而不是@media来定义容器查询。

 @container (min-width: 568px) { .card__wrapper { align-items: center; grid-gap: 1.5em; grid-template-rows: auto; grid-template-columns: 150px auto; } .card__image { min-width: auto; height: auto; } }

card__wrappercard__image元素都是card元素的子元素,它定义了contain属性。 当我们用容器查询替换常规媒体查询,移除窄容器的额外 CSS 类,并在支持容器查询的浏览器中运行 CodePen 示例时,我们得到以下结果。

在这个例子中,我们没有调整视口的大小,而是应用了 resize CSS 属性的 <section> 容器元素本身。该组件会根据容器尺寸自动在布局之间切换。
在这个例子中,我们没有调整视口的大小,而是应用了 resize CSS 属性的 <section> 容器元素本身。 该组件会根据容器尺寸自动在布局之间切换。 (大预览)

请参阅 Adrian Bece 的 Pen [产品卡:容器查询](https://codepen.io/smashingmag/pen/PopmQLV)。

请参阅钢笔产品卡片:Adrian Bece 的容器查询。

请注意,容器查询目前不显示在 Chrome 开发者工具中,这使得调试容器查询有点困难。 预计将来会在浏览器中添加适当的调试支持。

您可以看到容器查询如何使我们能够创建更健壮且可重用的 UI 组件,这些组件几乎可以适应任何容器和布局。 但是,该功能还需要对容器查询进行适当的浏览器支持。 让我们尝试看看我们是否可以使用渐进增强来实现容器查询。

渐进式增强和 Polyfills

让我们看看是否可以为 CSS 类变体和媒体查询添加回退。 我们可以使用带有@supports规则的 CSS 功能查询来检测可用的浏览器功能。 但是,我们无法检查其他查询,因此我们需要添加检查contain: layout inline-size style值。 我们必须假设支持inline-size属性的浏览器也支持容器查询。

 /* Check if the inline-size value is supported */ @supports (contain: inline-size) { .card { contain: layout inline-size style; } } /* If the inline-size value is not supported, use media query fallback */ @supports not (contain: inline-size) { @media (min-width: 568px) { /* ... */ } } /* Browser ignores @container if it's not supported */ @container (min-width: 568px) { /* Container query styles */ }

但是,这种方法可能会导致样式重复,因为容器查询和媒体查询都应用了相同的样式。 如果您决定使用渐进增强来实现容器查询,您可能希望使用 CSS 预处理器(如 SASS)或后处理器(如 PostCSS)来避免重复代码块,并改用 CSS mixins 或其他方法。

请参阅 Adrian Bece 的 Pen [产品卡:具有渐进增强的容器查询](https://codepen.io/smashingmag/pen/zYZwRXZ)。

请参阅 Pen 产品卡片:Adrian Bece 的渐进式增强容器查询。

由于此容器查询规范仍处于实验阶段,因此请务必记住,规范或实现在未来版本中可能会发生变化。

或者,您可以使用 polyfill 来提供可靠的回退。 我想强调两个 JavaScript polyfill,它们目前似乎得到了积极维护,并提供了必要的容器查询功能:

  • cqfill的cqfill
    用于 CSS 和 PostCSS 的 JavaScript polyfill
  • Chris Garcia react-container-query
    React 的自定义钩子和组件

从媒体查询迁移到容器查询

如果您决定在使用媒体查询的现有项目上实现容器查询,则需要重构 HTML 和 CSS 代码。 我发现这是添加容器查询的最快和最直接的方法,同时为媒体查询提供可靠的回退。 让我们看一下前面的卡片示例。

 <section> <div class="card__wrapper card__wrapper--wide"> <!-- Wide card content --> </div> </section> /* ... */ <aside> <div class="card__wrapper"> <!-- Narrow card content --> </div> </aside>
 .card__wrapper { display: grid; grid-gap: 1.5em; grid-template-rows: auto auto; /* ... */ } .card__image { /* ... */ } @media screen and (min-width: 568px) { .card__wrapper--wide { align-items: center; grid-gap: 1.5em; grid-template-rows: auto; grid-template-columns: 150px auto; } .card__image { /* ... */ } }

首先,将应用了媒体查询的根 HTML 元素包装为具有contain属性的元素。

 <section> <article class="card"> <div class="card__wrapper"> <!-- Card content --> </div> </article> </section>
 @supports (contain: inline-size) { .card { contain: layout inline-size style; } }

接下来,将媒体查询包装在功能查询中并添加容器查询。

 @supports not (contain: inline-size) { @media (min-width: 568px) { .card__wrapper--wide { /* ... */ } .card__image { /* ... */ } } } @container (min-width: 568px) { .card__wrapper { /* Same code as .card__wrapper--wide in media query */ } .card__image { /* Same code as .card__image in media query */ } }

虽然这种方法会导致一些代码膨胀和重复代码,但通过使用 SASS 或 PostCSS 可以避免重复开发代码,因此 CSS 源代码保持可维护性。

一旦容器查询获得适当的浏览器支持,您可能需要考虑删除@supports not (contain: inline-size)代码块并继续专门支持容器查询。

Stephanie Eckles 最近发表了一篇关于容器查询的精彩文章,涵盖了各种迁移策略。 我建议查看它以获取有关该主题的更多信息。

用例场景

正如我们从前面的示例中看到的那样,容器查询最适合用于高度可重用的组件,其布局取决于可用的容器空间,并且可以在各种上下文中使用并添加到页面上的不同容器中。

其他示例包括(示例需要支持容器查询的浏览器):

  • 模块化组件,如卡片、表单元素、横幅等。
  • 适应性布局
  • 具有不同功能的移动和桌面分页
  • 有趣的 CSS 调整大小实验

结论

一旦规范在浏览器中实施并得到广泛支持,容器查询可能会成为改变游戏规则的功能。 它将允许开发人员在组件级别编写查询,将查询移近相关组件,而不是使用遥远且几乎不相关的视口媒体查询。 这将导致更健壮、可重用和可维护的组件能够适应各种用例、布局和容器。

就目前而言,容器查询仍处于早期的实验阶段,实施很容易发生变化。 如果您现在想在项目中开始使用容器查询,则需要使用带有特征检测的渐进增强或使用 JavaScript polyfill 来添加它们。 这两种情况都会在代码中产生一些开销,因此如果您决定在此早期阶段使用容器查询,请确保计划在该功能得到广泛支持后重构代码。

参考

  • David A. Herron 的“容器查询:快速入门指南”
  • “向 CSS 容器查询问好”,Ahmad Shadeed
  • “Chrome 52 中的 CSS 包含”,Paul Lewis
  • “使用 CSS 包含属性帮助浏览器优化”,Rachel Andrew