别把 CSS 容器查询当成传统媒体查询
说实话,CSS Container Queries(容器查询)刚发布时,我完全没留意到。等终于听说它时,第一个念头竟是:“既然已经有媒体查询了,我到底为啥需要这个?”
现在回想起来,我对当时的反应有点羞愧,但得知有这种想法的不止我一个时,心里倒是宽慰不少——事实上,这样的开发者大有人在。
让我费解的是,容器查询并非什么新功能,目前它的浏览器支持率已达94%,可实际使用者却寥寥无几。根据《CSS现状调查报告》,86%的开发者知道容器查询,但只有41.4%真正在用。虽说调查可能存在偏差,无法完全代表整个行业,但它无疑是我们手头最具参考性的数据。
Kevin Powell在2026年阿姆斯特丹SmashingConf大会上也提到了这一点:容器查询的普及情况糟透了。而这让我倍感诧异,要知道,“让组件能根据外部容器尺寸自适应”的需求,多年来一直高居CSS功能心愿单榜首。
相比有多少人在用容器查询,我更关心大家怎么用它。我无法代表所有人,但就我所见——包括自己最初的尝试——很多人都用错了。
归根结底,误用的根源和我最初的想法如出一辙:乍一看,它们和媒体查询简直一模一样。因为外观相似,人们很容易想当然地认为二者用途、原理都相同。
但事实并非如此。
注:本文重点讨论的是容器尺寸查询,即一种根据特定容器尺寸调整样式的响应式设计技术。此外还有容器样式查询,它会根据容器的计算样式做出响应(本文撰写时该功能仍处于实验阶段)。关于样式查询的应用场景,可参考Juan Diego发表在Smashing Magazine上的相关文章。
媒体查询:向外看
视口一直是个“代理”。媒体查询让我们产生一种错觉,仿佛屏幕宽度就是决定应用如何适配环境的唯一因素。
不妨问问自己:写@media时,你在向浏览器询问什么?
@media (min-width: 1024px) {
.card {
display: flex;
}
和大多数开发者一样,我其实是在问:*现在屏幕宽度是多少?*仅此而已。
媒体查询能完美回答这个问题,但如果把这个.card组件放在1920px宽的桌面屏幕上、仅300px宽的网格单元格里,会发生什么?
媒体查询对此毫不在意,它只会按指令执行:视口宽度仍是1920px,所以min-width: 1024px的条件触发,匹配的样式被应用——哪怕卡片只有300px的可用空间。最终,卡片内容要么变形,要么溢出,要么挤成一团。
“媒体查询有点‘傻’。不是说概念本身傻,而是它能获取的信息太少了。事实上,大多数人都高估了它的能力。”
人们常把响应式设计单纯理解为整页布局的切换系统,比如大屏显示两栏、小屏显示单栏。
容器查询:向内看
容器查询要聪明得多。它让响应式布局更关注组件内部的状态,而非对组件内容一无所知的外部环境。它更像是在问:此刻,我在这个特定位置有多少可用空间?
下面是上一节中卡片代码的容器查询版本:
.card-wrapper {
container-name: card;
container-type: inline-size;
}
@container card (min-width: 450px) {
.card {
display: flex;
flex-direction: row;
}
}
这彻底改变了规则。卡片不再受视口影响,它只关心父容器.card-wrapper是否有至少450px的内联(即左到右书写模式下的水平方向)空间。如果满足条件,组件就变为水平布局;否则,就回到默认的块级显示方式。
可查看Vayo的CodePen示例:视口查询 vs 容器查询 [forked]。
“宏观”布局 vs “微观”布局
区分@media和@container查询的一个有趣角度是布局类型。
媒体查询用于“宏观”布局,它向外观察:比如页面整体结构、通栏的页眉页脚、主网格布局、系统偏好设置(prefers-color-scheme)、设备能力(如触摸屏)等所有关乎整页结构的核心元素。
而容器查询则用于“微观”布局,也就是宏观布局内部的大部分内容。当内容需要根据分配到的空间自适应时,就该用它了。典型应用场景包括卡片、小组件、表单、导航栏等组件。
换句话说,可以简单理解为**“页面布局” vs “组件布局”**。
元素不该仅仅因为屏幕宽度超过768px这类抽象阈值,就自动切换到“平板尺寸”布局。相反,它应该在获得足够空间时才切换布局——不管这个空间是来自移动端视口,还是桌面端侧边栏。
如果你仍有疑虑,不妨想想:现代网页的视口尺寸多达2300余种,你真的能为每一种都做好适配吗?
“
这才是合理的设计逻辑。
示例:组件内的流式排版
响应式排版是很多人依赖媒体查询实现的功能。媒体查询搭配vw、vh这类相对视口的CSS单位,看起来非常适合根据屏幕尺寸调整字体大小:
.card-title {
font-size: clamp(100%, 1rem + 2vw, 24px);
}
但一旦把这个组件放到侧边栏这类与视口无关的环境中,问题就来了。因为我们把响应性绑定在了错误的参照点上,字体大小可能变得过大或过小。
容器查询有自己专属的CSS单位——cqi、cqw、cqb等。我们可以将这些单位与CSS的clamp()函数结合,实现流式排版,让字体大小随容器而非视口变化:
.card-title {
font-size: clamp(1rem, .5rem + 3cqi, 2rem);
}
可查看Vayo的CodePen示例:流式排版 [forked]。
这样一来,所有代码都与元素所在的特定容器绑定,实现了完全的组件内自洽。
示例:Flexbox换行检测
有意思的是,容器查询还能在某种程度上检测组件内部布局的状态。它并非万无一失,但这是媒体查询绝对做不到的——因为媒体查询只能观察外部浏览器窗口,对内部布局事件(比如flex项换行)完全“视而不见”。我们来具体看看。
Flexbox的自动换行功能(flex-wrap: wrap)非常实用,当容器空间不足以容纳所有flex项时,它们会自动换行。但CSS本身无法判断换行何时发生,既没有:wrapped这样的伪类,也没有@media (flex-wrapped: true)这样的媒体查询。
媒体查询只能观察浏览器窗口,无法感知内部变化,而flex换行本质上就是元素自身的宽度状态变化。
比如,你有一个用flexbox实现的水平菜单,希望菜单项仅在换行时改变样式,通常需要借助JavaScript的ResizeObserver来实现。不过,我们可以把容器查询嵌套在flex项中,找到无需JavaScript的解决方案——这个技巧来自我从Kevin Powell那里学到的方法,核心思路是在查询容器内联尺寸时,让flex项开启flex-grow: 1。
可查看Vayo的CodePen示例:流式排版 [forked]。

具体逻辑如下:
- 空间充足时,两个flex项(
.flex-item)并排显示,各占父容器宽度的一半。 - 空间不足时,第二个项换行到下一行。
- 由于每个flex项都开启了
flex-grow,换行后的项会拉伸以填满父容器的大部分宽度。 - 如果flex项本身是容器,它会检测到宽度突然变大,从而触发容器查询。
/* flex父容器 */
.flex-layout {
display: flex;
flex-wrap: wrap;
}
/* 将flex项注册为容器 */
.flex-item {
container-type: inline-size;
flex: 1 1 390px; /* 空间充足时拉伸,宽度小于390px时换行 */
}
/* 默认卡片样式(窄屏/并排状态) */
.card {
display: flex;
flex-direction: column;
background: #f4f4f4;
}
/* 容器宽度足够显示整行时 */
@container (min-width: 600px) {
.card {
flex-direction: row;
align-items: center;
background: #e2f0d9;
}
}
这个方案可行。当父容器宽度缩小,卡片换行成两行时,卡片项会展开,容器查询触发并应用相应样式。
可查看Vayo的CodePen示例:用容器查询检测Flex换行 [forked]。
用容器查询检测Flex换行 [forked] 作者:Vayo
容器查询的副作用
和所有技术一样,容器查询也有一些需要注意的副作用或局限性。
1. 需要额外的容器层
容器查询需要类似父子关系的结构才能正常工作。比如我们有这样的标记:
<div class="card">
<div class="card-content">...</div>
</div>
我们不能直接通过查询.card组件来调整.card-content的样式,比如这样写:
/* 无法生效 */
.card {
container-name: card;
container-type: inline-size;
}
@container card (min-width: 400px) {
.card {
display: flex;
}
}
这样写行不通,因为容器不能查询自身。上面的代码试图根据card容器的尺寸来调整该容器的显示方式,会陷入无限循环。
正确的做法是添加一个额外的容器,让.card成为它的后代:
<div class="cards">
<div class="card">
<div class="card-content">...</div>
</div>
</div>
之后,我们就可以查询.cards容器,并据此调整.card的布局:
.cards {
container-name: cards;
container-type: inline-size;
}
@container cards (min-width: 400px) {
.card {
display: flex;
}
}
这一点在媒体查询中无关紧要,因为@media不关心你在代码块里给哪个元素设置样式,它只关注始终存在的视口。你可以直接给任意元素添加条件,轻松完成任务。
2. 查询容器size可能导致布局塌陷
当查询容器的size(即块级方向,也就是垂直方向的尺寸)而非inline-size时,就可能出现这种情况:
/* 即便内部有内容,高度也会塌陷为0px */
.hero-banner {
container-type: size;
}
为什么?因为浏览器计算容器尺寸时不会考虑其子元素。如果不给.hero-banner设置明确的height(或min-height、aspect-ratio),浏览器会将其高度设为0px。
因此,通常更好的做法是查询容器的inline-size,除非你确实需要查询块级方向的尺寸。
媒体查询不会受此影响,因为它处理高度的逻辑和@media (min-height: ...)一样,只需询问视口即可。
3. 查询条件不支持自定义属性
容器查询的另一个局限是:不能将自定义属性作为查询条件:
:root {
--breakpoint-lg: 1600px;
}
/* 无法生效 */
@container (min-width: var(--breakpoint-lg)) {
/* ... */
}
这是因为自定义属性的值依赖DOM树的样式继承。如果容器查询依赖某个自定义属性,而该查询又可能修改这个属性的值,情况会变得非常复杂:
:root {
--breakpoint-lg: 1600px;
}
/* 无法生效 */
@container cards (min-width: var(--breakpoint-lg)) {
.card {
--breakpoint-lg: 1000px;
}
}
何时用媒体查询,何时用容器查询
我认为任何项目都不应该完全用@container取代@media。媒体查询在响应式布局中仍扮演着重要角色,关键是要理清二者的职责边界。
当一个组件会在多种布局环境中使用时,我会选择容器查询。比如.card组件既可能放在全宽网格中,也可能放在较窄的侧边栏里。这种情况下,应该由组件内容决定何时调整布局,而非依赖观察外部视口的媒体查询。
反之,当组件仅在页面层级存在时,我会使用媒体查询。比如始终位于页面顶部的主导航栏,它直接受视口尺寸影响,此时视口就是判断导航栏何时需要调整的可靠参照。说到底,还是“宏观”布局与“微观”布局的区别。
下面是我判断该用哪种查询的决策流程图:

结语
归根结底,容器查询与媒体查询外观高度相似,只是为了贴合开发者的使用习惯。它算不上真正的“新”功能,但相比媒体查询,理解和使用它的人少得多。不过媒体查询本身存在诸多局限,否则我们也不需要容器查询来填补这些空白。
容器查询为我们提供了更有效的方式:检测特定组件的环境变化,并根据其内容调整样式——这正是多环境复用组件应有的设计逻辑。
(gg, yk)
(gg, yk)