← 返回文章

CSS / 前端工程 / 响应式设计

CSS 容器查询实战:让组件根据自己的空间响应

传统响应式页面通常以视口为中心:屏幕宽度小于 768px,就切换为移动布局;大于某个宽度,就展示更多列。这种方式适合控制整页结构,却很难解决组件复用问题。同一张商品卡片可能出现在首页主区域、侧边栏和弹窗中,视口宽度完全相同,但组件真正拥有的空间差别很大。

CSS 容器查询解决的正是这个问题。它允许组件根据某个祖先容器的尺寸切换样式,让响应式规则从“页面现在多宽”变成“组件现在有多少空间”。

媒体查询为什么不够用

先看一个常见写法:

1
2
3
4
5
6
7
8
9
10
.product-card {
display: grid;
gap: 16px;
}

@media (min-width: 960px) {
.product-card {
grid-template-columns: 180px 1fr;
}
}

当视口超过 960px,所有商品卡都变成横向布局。但如果一张卡片被放进只有 280px 宽的侧栏,它依然会执行横向规则,内容自然被挤坏。组件被迫知道自己会出现在哪些页面位置,业务代码里也会逐渐出现 .sidebar .product-card.dialog .product-card 这样的覆盖选择器。

容器查询把条件改为组件所在区域的宽度。页面可以自由编排,组件只关心自己能获得多少空间。

建立一个可查询容器

最常用的写法是给组件外层设置 container-type: inline-size

1
2
3
4
5
6
7
8
9
10
<section class="product-region">
<article class="product-card">
<img src="keyboard.jpg" alt="机械键盘" />
<div class="product-card__content">
<h2>低延迟机械键盘</h2>
<p>三模连接,支持热插拔。</p>
<button>查看详情</button>
</div>
</article>
</section>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.product-region {
container-type: inline-size;
}

.product-card {
display: grid;
gap: 12px;
padding: 16px;
border: 1px solid #d9dde5;
border-radius: 16px;
}

@container (min-width: 520px) {
.product-card {
grid-template-columns: minmax(160px, 36%) 1fr;
align-items: center;
}
}

inline-size 表示在文字行进方向上建立尺寸包含。对于常见的横向书写模式,可以先把它理解为“允许查询容器宽度”。相比同时约束宽和高的 size,它更适合大多数横向响应式组件,也更少影响正常布局。

需要注意,@container 中的 .product-card 会查找符合条件的祖先查询容器。不要期待一个元素既建立尺寸容器,又根据自己的尺寸在同一条规则里修改自己;通常应让外层负责提供查询环境,内层组件负责响应。

给容器命名,避免查询错层级

真实页面经常有多层容器。卡片内部可能还有工具栏,工具栏又有自己的响应式规则。如果只写匿名容器,浏览器会寻找最近且满足类型要求的祖先,这可能不是你心里想的那个区域。

可以使用 container-name 明确命名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.dashboard-main {
container-name: dashboard;
container-type: inline-size;
}

.toolbar-region {
container: toolbar / inline-size;
}

@container dashboard (min-width: 720px) {
.stats-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}

@container toolbar (max-width: 420px) {
.toolbar__label {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
}
}

container: toolbar / inline-size 是名称和类型的简写。命名的价值不只是“代码更清楚”,它还把组件依赖写进了样式契约:统计网格响应 dashboard,工具栏按钮响应 toolbar,不会因为中间多包了一层布局容器就悄悄改变行为。

用容器查询单位实现连续缩放

断点适合改变结构,但字号、间距和图标尺寸不一定要突然跳变。容器查询提供了 cqwcqhcqicqb 等单位。1cqw 等于查询容器宽度的 1%。

1
2
3
4
5
6
7
.hero-card__title {
font-size: clamp(1.25rem, 4cqw, 2rem);
}

.hero-card {
padding: clamp(16px, 5cqw, 40px);
}

clamp() 非常重要。单独使用 4cqw 可能在极窄容器中小到难以阅读,在超宽容器中又大得夸张。设置最小值和最大值,才能得到可控的连续变化。

容器单位也不应替代所有设计令牌。如果相同层级的卡片间距本来就要求一致,继续使用统一的 spacing token 会更稳。只有那些确实应该随局部空间变化的尺寸,才适合使用容器单位。

一个完整的可复用卡片策略

可以把组件分成三个状态,而不是为每个页面写一套皮肤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.card-slot {
container: card / inline-size;
}

.article-card {
display: grid;
gap: 12px;
}

.article-card__image {
inline-size: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 12px;
}

.article-card__summary {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}

@container card (min-width: 420px) {
.article-card {
grid-template-columns: 144px minmax(0, 1fr);
align-items: start;
}

.article-card__image {
aspect-ratio: 1;
}
}

@container card (min-width: 760px) {
.article-card {
grid-template-columns: 240px minmax(0, 1fr);
gap: 24px;
}

.article-card__summary {
-webkit-line-clamp: 3;
}
}

这个组件放进 320px 的侧栏时是纵向卡片,放进普通内容区时是缩略图加正文,放进宽阔区域时会获得更大的图片和间距。布局决策跟随实际空间,不依赖页面名称。

其中 minmax(0, 1fr) 是一个容易被忽略的细节。Grid 子项默认最小尺寸可能来自内容,长链接或不换行文本会把列撑开。把最小值明确设为 0,再为正文区域配置合适的换行策略,可以减少横向溢出。

1
2
3
4
.article-card__content {
min-inline-size: 0;
overflow-wrap: anywhere;
}

容器查询不等于“断点越多越好”

容器查询很方便,也可能被滥用。每个组件出现五六个局部断点后,设计系统会变得难以预测。更实用的做法是:

  • 先用 Grid、Flexbox、minmax()auto-fit 和内容换行完成自然布局;
  • 只有结构确实需要改变时才增加容器断点;
  • 断点来自内容开始拥挤的位置,而不是照搬某台设备的宽度;
  • 同一类组件共享少量状态,例如 compact、regular、wide;
  • 在最窄、断点前后和超宽状态分别做视觉检查。

容器尺寸也可能随内部内容变化。复杂嵌套中如果规则彼此影响,要避免制造难以理解的循环依赖。保持“外层分配空间,内层响应空间”的单向关系,通常最容易维护。

渐进增强与兼容策略

在需要照顾旧环境的项目里,可以把基础样式写成始终可用的单列布局,再通过特性查询增强:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.profile-card {
display: block;
}

@supports (container-type: inline-size) {
.profile-region {
container-type: inline-size;
}

@container (min-width: 480px) {
.profile-card {
display: grid;
grid-template-columns: 120px 1fr;
}
}
}

不支持容器查询的浏览器仍能得到可阅读的单列卡片,支持的浏览器获得更合适的组件布局。是否还要提供媒体查询回退,要看项目的浏览器基线和实际用户分布,而不是一概而论。

落地检查清单

把一个旧组件迁移到容器查询时,可以依次检查:

  1. 响应式条件真正依赖视口,还是依赖组件可用空间?
  2. 哪个外层最适合成为稳定的查询容器?
  3. 页面是否存在嵌套容器,需要显式命名?
  4. 能否先用现代布局完成自适应,只保留少量结构断点?
  5. 长标题、长链接、空内容和多语言文本是否会溢出?
  6. 基础样式在不支持容器查询时是否仍然可用?
  7. 组件在真实侧栏、弹窗和内容区中是否都经过检查?

媒体查询依然适合页面级布局和用户偏好,例如 prefers-reduced-motion;容器查询则更适合组件级布局。两者不是替代关系。把“页面如何编排”和“组件如何适应”分开,才是容器查询给前端架构带来的最大价值。

参考资料