CSS 教程 / 可复用组件的 CSS 容器查询

容器大小查询允许组件根据可查询的祖先容器尺寸做出响应,而不仅仅是视口。使用 container-type 声明查询容器,然后使用 @container 在适当的大小下更改子元素样式。

比较视口条件和容器条件

即使浏览器宽度为1200像素,根据卡片是在700像素的正文区域还是300像素的侧边栏中,其正确的位置也会有所不同。仅使用CSS媒体查询和基于内容的断点来表达这种差异是困难的,因为两个卡片共享相同的视口条件。在重用部件时,可以通过根据父级宽度更改它们的内部配置来减少特定布局的例外规则。

划分 判断标准 适当的角色
@media 视口宽度等环境 页面跨列数,侧边栏位置
@container 所选祖先容器的大小等。 卡片内图片和文本的放置,各部分之间的间距。

容器查询还会比较条件表达式的宽度,而不是“如果屏幕是水平的则水平布局”。当侧边栏查询宽度大于或等于 30rem 时,其内的卡片也可以水平布局。如果你总是想要一个窄的侧边栏,你还应该在父布局中指定最大宽度。

声明一个尺寸查询容器

.slot { container: panel / inline-size; }
@container panel (min-width: 30rem) {
  .card { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: 1rem; }
}

container: panel / inline-size 声明容器名 panel,并启用内联方向的尺寸隔离(inline-size containment)。在横排书写中,内联尺寸是水平的;竖排书写会改变这种对应关系。通过其外部块布局或网格轨道设置容器的宽度,以便卡片具有可查询的有意义的尺寸。参见 CSS 网格基础:轨道、线条和项目放置。

用于评估条件的父元素是 .slot,接收条件样式的子孙元素是 .card。不要通过查看卡片本身的尺寸来编写会改变卡片自身结构的代码。为了避免随着尺寸变化条件反复翻转的问题,尺寸查询应相对于合适的祖先元素进行操作。

inline-size 的包含使容器的内联尺寸不依赖于其子元素内容的贡献。因此,一个收缩适应的容器可能会变得意外地小。在这里,一个块或网格轨道提供了它的宽度。使用 size 的包含也会影响另一轴方向,并可能折叠一个按内容大小设定的高度,因此请谨慎选择包含方式。有关详细信息,请参见 MDN:container-type 与尺寸隔离范围 。

在不同的父元素宽度中重用一个卡片

将其保存为 container-card.html ,以 UTF-8 编码保存并在浏览器中打开。颜色框是表示图片区域的示例元素,因此没有单独的图像文件。在较大的屏幕上,将主要文本和宽度最多为 20rem 的侧边栏分开,在宽度为 44rem 或更小的屏幕上,将两个区域放在同一列中。

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>container-card.html</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font: 1rem/1.6 sans-serif; color: #243447; }
h1, h2, p { margin-top: 0; } a { color: #145f8f; }
a:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid #ac4100; outline-offset: 3px; }
.layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 20rem); gap: 1rem; width: min(100%, 75rem); margin-inline: auto; padding: 1rem; }
.slot { min-width: 0; container: panel / inline-size; }
.card { min-width: 0; padding: 1rem; background: #e8f3fa; overflow-wrap: anywhere; }
.media { min-height: 7rem; background: #287699; }
.card h2 { font-size: 1.25rem; } .card p { margin-bottom: 0; }
@container panel (min-width: 30rem) {
  .card { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: 1rem; align-items: start; }
}
@media (max-width: 44rem) { .layout { grid-template-columns: minmax(0, 1fr); } }
</style>
</head>
<body>
<div class="layout">
<main class="slot"><article class="card"><div class="media" aria-hidden="true"></div><div><h2>Card in the main content area</h2><p>The image and text appear side by side when the parent is at least 30rem wide.</p></div></article></main>
<aside class="slot"><article class="card"><div class="media" aria-hidden="true"></div><div><h2>The same card in a sidebar</h2><p>The sidebar in this example stays at or below 20rem even on a wide screen, so the card keeps its vertical layout.</p></div></article></aside>
</div>
</body>
</html>

在本例根元素字号为 16px 的默认环境中,30rem 等于 480px。如果宽屏布局中正文的 .slot 查询宽度大于或等于 480px,卡片内部将为两列;在宽度最多为 320px 的侧边栏中,卡片内部将垂直排列。在窄屏上,随着侧边栏下移,其父元素可能会变宽。例如在 600px 视口下,外层的正文与侧边栏区域上下排列,但两张卡片各自内部都可能呈现图文并排,这是正常结果。这里容器查询的 rem 按根元素的实际字号换算,不要套用媒体查询中使用初始字体值的规则。

44rem 的外部条件和 30rem 的内部条件解决不同的问题。第一个条件决定如何在整个页面上放置区域,第二个条件决定如何在分配给每张卡片的空间中放置图片和文字。每张卡片都从自己的祖先中选择最近且具备所需尺寸查询能力的 panel 容器,然后评估 30rem 条件;容器同名并不意味着查询的是同一个元素。

比较最近容器与命名容器

未命名的尺寸查询先选择最近且能查询相应尺寸轴的祖先容器,再评估宽度条件。若该容器不足 30rem,条件为假,不会继续向外寻找一个更宽的容器。即使外部区域很宽,如果中间添加了一个较窄的容器,结果可能会有所不同。通过将以下示例保存为 container-nested.html,你可以单独看到这种差异。

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>container-nested.html</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font: 1rem/1.6 sans-serif; color: #243447; }
h1, h2, p { margin-top: 0; } a { color: #145f8f; }
a:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid #ac4100; outline-offset: 3px; }
main { width: min(100%, 42rem); padding: 1rem; }
.outer { width: 100%; container: outer / inline-size; border: 2px solid #789; padding: 1rem; }
.inner { width: min(100%, 17rem); container-type: inline-size; }
.card { padding: 1rem; background: #e8f3fa; overflow-wrap: anywhere; }
.media { min-height: 5rem; background: #287699; }
@container (min-width: 30rem) {
  .card { display: grid; grid-template-columns: 5rem minmax(0, 1fr); gap: 1rem; }
}
</style>
</head>
<body>
<main><h1>Which parent's width is queried?</h1><div class="outer"><div class="inner"><article class="card"><div class="media" aria-hidden="true"></div><p>The nearest inner container has a maximum of 17rem.</p></article></div></div></main>
</body>
</html>

在宽屏上,外部容器大于30rem,但内部容器最多为17rem。未命名的条件选择最近的内部元素,因此卡片垂直排列。保留其他代码,并将@container (min-width: 30rem)替换为以下内容:

@container outer (min-width: 30rem) {
  .card { display: grid; grid-template-columns: 5rem minmax(0, 1fr); gap: 1rem; }
}

现在我们选择名为outer的外部祖先,因此在宽屏上卡片水平排列。卡片本身仍在17rem的最大值内,因此你需要单独检查主体宽度是否足够。容器名用于筛选查询对象,不会自动修正不合适的布局宽度。存在多个同名且适合的祖先时,仍选择最近的一个,再判断尺寸条件。比较后,恢复为未命名的条件。

诊断不适用的查询

  1. 检查条件选择器的目标是否是容器的后代。确保你没有试图将相同的样式应用到父元素本身。
  2. 在开发者工具中检查父元素的容器类型和实际内容宽度。应区分包括内边距和边框的外部尺寸与查询所用的内容宽度。
  3. 检查是否创建了新的容器,必要时用 container-name 筛选查询容器。
  4. 如果仅将 .slot 的 container-type 改为 normal,你可以看到尺寸查询未被应用的区别。确认后,将其恢复为 inline-size。

保持可读的垂直卡片作为基线,然后在容器查询适用时添加水平布局。基线同样适用于不支持尺寸查询的浏览器。检查所需浏览器的支持情况和行为。请参阅 MDN:容器尺寸和样式查询 。