过去十年,前端开发者做响应式设计时手里只有一件工具:媒体查询。它确实好用,但有一个永远绕不开的痛点——媒体查询只能问视口多宽多高,从来不知道某个组件自身的宽度是多少。当同一个组件被放在侧边栏、主内容区和弹窗中时,你需要写三套不同的类名或者用复杂的CSS变量来适配,因为组件本身对它被放在哪里一无所知。
CSS容器查询正是为了终结这个局面而生的。它让组件能够感知直接父容器的尺寸,并根据容器的宽度、高度甚至内联尺寸来调整样式。这意味着你可以为组件写一套自适应规则,扔到任何布局区域里都能正常工作。Chrome 105、Edge 105、Safari 16、Firefox 110都已经完整支持,生产环境可以放心用了。
这篇文章会把容器查询的语法、逻辑和边界情况彻底讲清楚,最后用一个完整的卡片组件案例演示它怎么让响应式代码变得干净利落。
容器查询解决了什么问题
先看一个实际问题。你设计了一个商品卡片组件,需求是:卡片宽度超过350px时,内部布局变成水平排列(图片在左,文字在右);宽度不足350px时,堆叠排列(图片在上,文字在下)。用媒体查询怎么做?
你会发现做不到,因为媒体查询只能检测视口宽度。如果卡片被放在一个800px宽的主区域里,视口可能是1200px,永远触发水平布局;如果同一张卡片放到一个250px宽的侧边栏里,它还是按照1200px视口规则走,直接撑破布局。你只能给卡片加父级相关的类名,比如.sidebar .card和.main .card,样式耦合得面目全非。
容器查询让卡片自己“问”父容器:你现在有多宽?如果大于350px,我就水平排列;否则堆叠。这样卡片组件就成了一个独立的自适应单元,与外部布局解耦。
基础语法三步走
启用容器查询需要做三件事:声明容器、命名容器、写容器查询规则。下面一步步拆开。
第一步:把元素变成容器
默认情况下,任何元素都不是容器。需要通过container-type属性显式声明。最常用的值是inline-size,表示容器在行内方向(对于横排文本就是宽度)的尺寸变化会被查询。
.card-wrapper {
container-type: inline-size; /* 让这个元素成为容器,基于宽度查询 */
}
如果还想同时查询高度,用container-type: size。但要注意,如果同时查询高度和宽度,必须给容器指定一个确定的块级尺寸(比如设置height),否则高宽都是0或者auto会导致查询失灵。大多数场景下inline-size就够了。
声明container-type之后,这个元素就变成了一个容器上下文,它的子元素可以使用容器查询来响应它的尺寸变化。同时,容器元素自身会创建新的包含块,行为和overflow非visible时类似,但不会裁剪内容。
第二步:给容器起个名字(可选)
一个页面里可能有多个不同的容器,你只想让卡片组件响应名为card-container的容器,而不是它遇到的任何一个容器。这时候用container-name。
.card-wrapper {
container-type: inline-size;
container-name: card-container;
}
/* 简写形式 */
.card-wrapper {
container: card-container / inline-size;
}
如果不需要名字,直接用container-type就行,子元素在写容器查询时可以不指定名字,自动查找最近的一个容器祖先。
第三步:用@container写查询规则
语法和媒体查询几乎一模一样,只是把@media换成了@container,尺寸条件同样支持min-width、max-width、min-height等。
@container card-container (min-width: 350px) {
.card {
display: flex;
flex-direction: row;
}
}
@container card-container (max-width: 349px) {
.card {
display: flex;
flex-direction: column;
}
}
如果不关心容器名字,只查找最近的容器,可以省略名字:
@container (min-width: 350px) {
.card {
flex-direction: row;
}
}
这样.card元素会沿着DOM树向上找第一个声明了container-type的祖先,用那个容器的尺寸来评估条件。
容器查询长度单位:cq系列
媒体查询有vw、vh,容器查询自然也有专属单位。这些单位让你的样式可以直接基于容器的尺寸来计算,而不依赖视口。
cqw:容器宽度的1%。如果容器宽度400px,1cqw = 4px。cqh:容器高度的1%。cqi:容器行内尺寸的1%(对于从左到右的文本就是宽度)。cqb:容器块级尺寸的1%(通常是高度)。cqmin:容器宽度和高度的较小值的1%。cqmax:容器宽度和高度的较大值的1%。
这些单位让字体大小、间距、图片尺寸可以跟着容器等比缩放,而不需要复杂的百分比计算。一个典型用法:
.card-title {
font-size: clamp(1rem, 5cqi, 2rem);
/* 字体大小在容器宽度的5%上下浮动,但限制在1rem到2rem之间 */
}
这里cqi比cqw更科学,因为它考虑了书写模式,在竖排文本场景下也能正常工作。日常用cqi替代cqw是个好习惯。
完整案例:自适应卡片组件
下面构建一个完整的卡片组件,演示容器查询如何让一个组件在不同尺寸的父容器中自动调整内外布局。卡片包含一张图片、一个标题、一段描述和一个按钮。需求如下:
- 容器宽度大于500px时,卡片整体采用水平布局,图片占40%宽度,文字区在右,标题字号加大。
- 容器宽度在300px到500px之间时,卡片仍水平但图片缩小到30%,标题字号适中。
- 容器宽度小于300px时,卡片堆叠布局,图片在上,文字在下,标题恢复到基准大小,按钮全宽。
HTML结构非常简单,一个外层包装器声明为容器,里面放卡片:
<div class="card-container">
<article class="card">
<img class="card-image" src="https://placehold.co/600x400" alt="产品图">
<div class="card-body">
<h2 class="card-title">轻量徒步背包</h2>
<p class="card-desc">防水面料,35L容量,适合一日短途穿越。</p>
<button class="card-btn">查看详情</button>
</div>
</article>
</div>
CSS部分,先把容器声明好,然后针对不同断点写容器查询。注意这里所有响应式代码都在@container里,不再使用任何媒体查询。
/* 容器声明 */
.card-container {
container-type: inline-size;
container-name: card-container;
}
/* 基础样式(最小屏幕/容器时的默认堆叠) */
.card {
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
background: #fff;
}
.card-image {
width: 100%;
height: auto;
object-fit: cover;
aspect-ratio: 3 / 2;
}
.card-body {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.card-title {
font-size: 1.2rem;
margin: 0;
}
.card-desc {
margin: 0;
color: #555;
line-height: 1.5;
}
.card-btn {
padding: 10px 16px;
border: none;
background: #1a6f59;
color: white;
border-radius: 8px;
cursor: pointer;
}
/* 容器宽度300px到499px:水平布局,图片稍窄 */
@container card-container (min-width: 300px) and (max-width: 499px) {
.card {
flex-direction: row;
}
.card-image {
width: 30%;
aspect-ratio: 1 / 1;
}
.card-body {
flex: 1;
}
.card-title {
font-size: 1.1rem;
}
}
/* 容器宽度大于500px:水平布局,图片宽,标题大 */
@container card-container (min-width: 500px) {
.card {
flex-direction: row;
}
.card-image {
width: 40%;
aspect-ratio: 4 / 3;
}
.card-body {
flex: 1;
justify-content: center;
}
.card-title {
font-size: 1.6rem;
}
.card-btn {
align-self: flex-start;
}
}
/* 容器宽度小于300px时:加强按钮全宽 */
@container card-container (max-width: 299px) {
.card-btn {
width: 100%;
text-align: center;
}
}
现在这个卡片可以放在任何地方:主内容区、侧边栏、网格项,甚至一个可拖拽缩放的容器里。它完全依据.card-container的宽度改变自己的形态,而不是视口宽度。如果你在页面上同时放了三个不同宽度的容器,每个容器里的卡片都会各自适配,互不干扰。
为了测试效果,你可以给.card-container加上resize: horizontal; overflow: auto;,然后直接在浏览器里拖拽容器边缘,观察卡片实时变化。这种体验是媒体查询给不了的。
容器查询 vs 媒体查询:什么时候用哪个
容器查询不是用来替代媒体查询的,两者解决的是不同层面的问题。媒体查询回答“页面整体在什么设备/窗口下”,适合做页面级布局切换(比如从两栏变一栏),以及全局字体大小、主题切换等。容器查询回答“组件在什么空间里”,适合组件内部样式的自适应。
一个健康的CSS架构往往是顶层用媒体查询处理页面结构,组件层用容器查询处理内部样式。这两者可以完美共存。比如整个页面的侧边栏是否展开由媒体查询决定,而侧边栏里的卡片怎么显示由容器查询决定。
嵌套容器与查询优先级的处理
当存在多层容器嵌套时,子元素在写@container时默认会查找最近的祖先容器。如果你想显式指定某个具名容器,就在@container后面跟上名字。如果不带名字,就查最近那个有container-type的祖先。
举个例子,页面最外层有一个main-container,里面嵌套了一个card-wrapper作为卡片容器,卡片内部又用容器查询。卡片内的元素如果写@container (min-width: 500px),它找的是最近的容器祖先,也就是card-wrapper,而不是更外层的main-container。这种就近原则符合直觉,也保证了组件的独立性。
如果同名容器嵌套,容器查询会匹配同名且最近的那个。一般建议给不同用途的容器起不同的名字,避免混淆。
性能与限制
容器查询在浏览器中是高效实现的,它利用了布局缓存,只在容器尺寸真正变化时才重新评估@container规则。但需要注意,声明container-type的元素本身不能是行内元素(inline),如果给span设置container-type: inline-size,它会先变成块级容器,可能会破坏原有布局。建议用div或语义化块级元素做容器。
另外,容器的尺寸受到自身CSS盒模型的影响。如果容器本身的尺寸是靠内容撑开的(比如没有设置width,由子元素宽度决定),那么当子元素样式因为容器查询而改变时,可能会导致容器宽度来回抖动——子元素变宽撑大容器,容器变宽触发新的查询,子元素再变窄……这种循环在极端情况下会引发布局抖动。浏览器有防护机制(不允许在同一帧内无限递归样式计算),但设计时最好避免“子元素尺寸影响容器尺寸,容器尺寸又反过来影响子元素”的死循环。通常给容器一个确定的宽度,或者用flex/grid让容器尺寸由父级决定,就能避开这个坑。
浏览器支持与降级策略
截至2025年,所有现代浏览器都支持容器查询。对于少数尚未升级的用户,降级方案也很简单:先写一套不依赖容器查询的基础样式(我们案例里已经写了堆叠布局作为基础),然后使用@supports (container-type: inline-size)包裹容器查询的高级规则。不支持的浏览器会忽略@container块,只显示基础样式,功能完全可用,只是响应式效果差一些。
@supports (container-type: inline-size) {
@container card-container (min-width: 500px) {
.card { flex-direction: row; }
}
}
这样即使老浏览器也能正常显示卡片,只是固定为堆叠布局而已,不会出现布局错乱。
不仅仅是宽度:样式查询的展望
容器查询目前只支持尺寸查询(inline-size, size, width, height等)。CSS工作小组已经在设计“样式查询”——允许根据容器自身某个CSS属性的值来做条件判断,比如@container style(--theme: dark)。这个特性在Chrome Canary中已经可以通过flag开启,但距离正式发布还有一段时间。那时候容器查询的能力将更加夸张,组件可以基于容器传递的任意自定义属性来切换形态,真正实现样式层面的独立封装。
回到当下,容器查询已经足够改变我们书写响应式CSS的方式。从今天开始,当你写组件样式时,试着把条件判断从“视口多宽”切换到“这个组件所在的容器多宽”,你会发现样式表里那些冗长的父级选择器和耦合的类名突然都消失了。

