从媒体查询到容器查询:组件为什么不能只认识“视口”
先从一个经常遇到的场景说起。设计稿里有一套卡片组件,它在 360px 宽的容器里是上下结构,到 480px 时变成左右结构。以前的做法很简单,依据视口宽度写两套媒体查询。但只要这个卡片被放到侧边栏、弹窗或网格单元里,整个逻辑立刻不成立了。

这不是很少见的边角情况。今天的页面里,同一个组件出现在不同宽度区域是常态。一个卡片组件可能在 240px 的工具栏里、在 800px 的主内容区里、又在 500px 的弹窗里出现。媒体查询判断的是浏览器窗口,根本感知不到卡片实际所在的容器有多宽。这也是 CSS Container Queries 出现的直接原因:让组件有能力根据真实的容器尺寸来做响应式适配。
容器查询的核心语法与工作机制
容器查询的思路是,先声明一个容器,然后在容器内部使用 @container 规则查询这个容器的尺寸,从而控制子孙元素的样式。它不像媒体查询那样只能听“屏幕”的,而是能听“祖父母”的。
/* 声明一个容器 */
.widget-wrapper {
container-type: inline-size;
}
/* 在容器查询内部写样式 */
@container (min-width: 500px) {
.widget {
display: grid;
grid-template-columns: 1fr 2fr;
}
}
这个例子里,container-type: inline-size 表示只跟踪宽度方向。使用 inline-size 而不是 size,是因为高度跟踪往往会让浏览器付出额外的布局成本,而且容易引发循环依赖。在多数界面场景中,我们只需要知道“相对宽度”就够了。
当组件嵌套比较深,页面上可能存在多个容器时,可以用 container-name 给容器命名,避免查询到错误的祖先。例如:
.main-panel {
container-type: inline-size;
container-name: panel;
}
@container panel (min-width: 600px) {
.data-card__title {
font-size: 1.5rem;
}
}
不写名字时,@container 会向上寻找最近的祖先容器。这里有个容易让人迷惑的地方:容器查询只能查询祖先容器,不能查询自身,也不能查询兄弟节点。
容器查询和媒体查询的真实分工
看到容器查询在工作,很多团队会立刻想把所有媒体查询替换掉。但我建议冷静一点。媒体查询解决的是页面级问题,容器查询解决的是组件级问题,二者不是同类工具。页面骨架的响应式,比如侧边栏是否折叠、主内容区是单栏还是双栏,仍然应该由视口宽度驱动。组件内部的排版、密度、图片表现,则应该交给容器查询。
把它们混在一起,可能会出现两种麻烦:如果大小页面级布局,会让容器查询承担全局判断,却无法感知整体视口;如果大小组件级样式,会重蹈以前“每个组件写一堆媒体查询”的覆辙。理想的方式是分而治之:
| 维度 | 媒体查询 | 容器查询 |
|---|---|---|
| 判断依据 | 视口或视口方向 | 祖先容器的尺寸或样式 |
| 作用范围 | 整个页面 | 容器自身的子树 |
| 典型场景 | 全局导航、页面分栏、全局字体层级 | 卡片排版、表格列数、图表自适应、表单布局 |
| 响应来源 | 浏览器窗口大小变化 | 父容器尺寸变化(布局调整、拖拽、动态内容) |
| 兼容性现状 | 所有现代浏览器 | 自 2023 年起主流浏览器支持 |
| 性能影响 | 窗口尺寸变化触发全页面重排 | 容器尺寸变化影响该容器内子树,粒度更小 |
这张表想说明的核心是:页面级断点不要硬塞给容器查询,组件级细节也不要奢望用媒体查询解决。两者配合使用,才能构建出既稳定又灵活的响应式体系。
实战场景:Dashboard 中的卡片终于不靠 JS 了
容器查询最让人有感的场景,是可拖拽布局。以前做 Dashboard,面板大小由用户拖拽控制,内部卡片往往需要根据面板宽度调整列数。没有容器查询时,你只能通过 ResizeObserver 监听每个面板的宽度,然后给内部元素设置一个或多个 data 属性,再写对应样式。这个过程繁琐,而且 JS 和 CSS 的边界变得很模糊。
有了容器查询,面板本身就可以是容器。内部卡片、表格、图表直接响应面板宽度。拖拽过程中,浏览器可以连续同步更新样式,不需要额外写监听逻辑。组件也变得更“聪明”——它在任何宽度下都知道如何调整自己的展现方式。
另一个典型场景是设计系统。你的团队维护着一个复杂的组件库,同一个产品列表组件会被用在侧边栏、筛选弹窗、结果列表里。过去,你需要为这些不同位置写好几套媒体查询,或者给组件传入不同 props。现在,你只需要在组件根节点上声明 container-type,然后按照容器宽度定义几个布局档位,组件就能在所有位置保持一致的自适应逻辑。
避免踩坑:容器查询的四个常见误区
容器查询语法简单,但真正迁移时容易在细节上出问题。下面这几点值得你提前留意。
- 只能查询祖先,不能查询自己。 不要在组件的根元素上既声明自己是容器,又用
@container查询自己,那不会生效。要查询的应该是包裹组件的那个父元素。 - 不要只盯着
size用。container-type: size会让容器尺寸脱离内容,必须显式指定宽高,还可能导致高度循环。多数场景用inline-size就够了。 - 注意 containment 副效应。 声明容器会附加 CSS Containment,子元素的行为可能发生变化,比如绝对定位元素不再以视口为参考,而可能受包含块影响。这需要在实际布局里测试,不能只凭直觉。
- 容器粒度别太大。 一旦容器大小变化,整个容器子树都会参与样式重算。如果把半个页面都设为容器,性能优势反而变成负担。合理的做法是在需要动态变化的局部区域创建容器。
渐进增强与落地建议
虽然容器查询支持度已经不是问题,但稳妥落地仍然建议采用渐进增强。先写一套不依赖容器查询的默认样式,再用 @supports 和 @container 做增强。这样即便遇到不支持的老浏览器,页面也不会崩。
/* 默认窄尺寸样式 */
.media-card {
display: block;
}
/* 只有支持容器查询时,才启用组件级响应 */
@supports (container-type: inline-size) {
.media-list {
container-type: inline-size;
container-name: media;
}
@container media (min-width: 480px) {
.media-card {
display: grid;
grid-template-columns: 160px 1fr;
}
}
@container media (min-width: 800px) {
.media-card {
grid-template-columns: 240px 1fr;
}
}
}
为什么要保留默认样式?因为容器查询的核心是“组件根据容器宽度自适应”,但默认情况下,一个元素没有声明容器时,宽度就是父容器的宽度。直接在默认块状排版上进行增强,即使容器查询不生效,视觉上也是可以接受的。
如果你准备在一个已有的中大型项目里引入容器查询,我建议按下面的节奏来:
- 先挑两三个复用率高、宽度变化明显的组件,比如卡片、表格、图表容器,做小范围验证。
- 在组件规范里明确:页面级响应交给媒体查询,组件级响应交给容器查询,不允许跨层混用。
- 把
@supports作为标准模版写入公共样式或代码片段,降低团队接入成本。
最后补充一个容易被忽略的点:容器查询还带来了新的 CSS 单位,比如 cqw、cqh、cqi、cqb,分别代表容器宽高、内联轴块轴的一定比例。它们可以配合容器查询使用,比如让子元素字号相对于所在容器宽度呈比例变化。这些单位在实现“容器内百分比”时非常实用,不过因为优先级和浏览器实现还在完善,建议先小范围测试。
总结:组件级响应式设计的原生产物
回过头看,CSS Container Queries 解决的其实是一个长期存在于组件化开发里的“归属感”问题:组件到底该听谁的?以前只能听屏幕的,现在可以听容器的。它让组件的表现与组件自身所处的环境强绑定,而不是与整个页面的视口绑定。
当然,它不是银弹。媒体查询依然有不可替代的值,容器查询也会带来新的复杂度。但方向已经很明确:响应式设计的单位正在从“页面”进一步下沉到“组件”。对做设计系统、复杂业务前端以及大型组件库的团队来说,把容器查询引入到你的组件规范里的时机,基本已经成熟。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/774/