过去十年,前端开发者做响应式设计时脑子里只有一件事——视口宽度。媒体查询@media撑起了整个移动互联网时代的适配方案,但它一直有一个没法绕开的痛点:组件的样式只跟屏幕大小挂钩,却不知道自己在页面里实际占了多少空间。一个在宽屏上横排的卡片列表,放到侧边栏里就该竖排,但媒体查询感知不到这个上下文变化。容器查询(Container Queries)的出现,终于把响应式的粒度从页面级推到了组件级。这篇文章会通过搭建一套完整的自适应卡片组件库,把容器查询的语法、踩坑经验和落地策略讲清楚。
一、为什么媒体查询不够用了
先看一个每天都在发生的场景。你写了一个产品卡片组件,在桌面端主内容区是三列布局,每张卡片的图片在左、文字在右,横着展示。产品经理说这个卡片要在后台仪表盘的右侧边栏里也复用,但侧边栏宽度只有三百多像素,横排显然放不下,图片和文字得改成上下堆叠。
用媒体查询怎么解决?没法直接解决。媒体查询只能根据视口宽度切换样式,而同一个视口下,主内容区和侧边栏的宽度天差地别。过去的方案是给组件加个修饰类,比如.card--compact,然后用JavaScript监听侧边栏的尺寸变化,动态添加或移除这个类。要么就干脆写两套CSS,一套给宽容器用,一套给窄容器用,靠父级选择器硬耦合布局上下文。
这两种方案维护起来都让人头疼。组件的样式本该内聚在自己的作用域里,现实却不得不向外窥探父容器的状态。容器查询就是来终结这个局面的——它让组件能根据自身所在容器的尺寸,而不是视口的尺寸,来决定内部元素的排列方式。组件被放到宽容器里就自动展开,被塞进窄容器里就自动收缩,不用改一行HTML结构,也不用写任何JavaScript。
二、容器查询的核心语法拆解
容器查询的语法分成两步:先定义容器,再写查询条件。这两步涉及几个新的CSS属性和规则,下面逐一拆开。
2.1 用container-type声明一个查询容器
想让一个元素成为”容器”(即子组件可以感知它的尺寸),必须显式声明container-type属性。这个属性有三个可选值:
inline-size:只建立内联尺寸(通常是宽度,在横向书写模式下)的查询容器。大多数场景用这个就够了,性能开销也最小。size:同时建立宽度和高度的查询容器。只有在组件需要根据容器高度变化调整样式时才用。normal:默认值,不建立查询容器。
声明容器时通常还会搭配container-name给容器起个名字,方便在查询条件里精确指定要监听哪个容器。写成合写属性更简洁:
/* 完整写法 */
.card-wrapper {
container-type: inline-size;
container-name: card-container;
}
/* 合写属性,第一个值是名称,第二个是类型 */
.card-wrapper {
container: card-container / inline-size;
}
一个页面上可以有多个命名容器,嵌套的容器也不会互相干扰。如果某个容器不需要名字,只想匿名使用,可以省略container-name,子组件在查询时会自动向上查找最近的匹配容器。
2.2 @container规则:写在组件内部的查询条件
有了容器之后,就可以在组件的CSS里写@container规则了。语法跟@media非常像,读起来几乎没有学习门槛:
/* 监听最近的匿名或命名容器,当容器宽度大于等于600px时应用内部样式 */
@container (min-width: 600px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 16px;
}
}
/* 指定只监听名为card-container的容器 */
@container card-container (max-width: 400px) {
.card {
display: flex;
flex-direction: column;
}
}
@container的条件表达式支持min-width、max-width、min-height、max-height,也支持逻辑运算符and、or和not。但要注意,容器查询的尺寸只能基于查询容器的内容区域(content box),不能引用padding或border区域,这一点和媒体查询基于视口的行为有所不同。
2.3 容器查询长度单位:cqw / cqh / cqi / cqb
光有条件判断还不够,容器查询还引入了一组新的相对单位,让组件内部的尺寸可以直接参照容器的尺寸来计算:
cqw:容器宽度的1%。cqh:容器高度的1%。cqi:容器内联尺寸(横向书写模式下等于宽度)的1%。cqb:容器块级尺寸(横向书写模式下等于高度)的1%。cqmin:容器宽度和高度中较小值的1%。cqmax:容器宽度和高度中较大值的1%。
这组单位的意义在于,组件的字体大小、间距、图片尺寸终于可以根据容器的实际空间来按比例缩放了。一个标题在宽容器里用2rem很合适,但在窄容器里可能太大,用clamp(1rem, 5cqi, 2rem)就能让标题字号跟随容器宽度平滑变化,不需要写一堆断点。
三、实战:构建一个自适应卡片组件库
理论讲完了,接下来动手做一个完整的例子。我们要构建的卡片组件会在三种不同宽度的容器中自动切换布局,同时字体、间距、图片比例也会跟着容器尺寸微调。整个过程不写一行JavaScript,所有变化都由容器查询驱动。
3.1 HTML结构:把卡片放进不同大小的容器里
先搭建页面的骨架。准备三个section作为不同宽度的容器,每个容器里放一张结构完全相同的卡片。注意容器上要声明container-type,这一步少了后面所有的查询都不会生效。
<!-- 宽容器 —— 模拟主内容区 -->
<section class="container-demo container-wide">
<h2>宽容器(700px以上)</h2>
<div class="card-wrapper">
<article class="card">
<div class="card__image">
<img src="placeholder.jpg" alt="产品示意图">
</div>
<div class="card__body">
<h3 class="card__title">自适应卡片标题会随容器缩放</h3>
<p class="card__desc">这段描述文字的字体大小和行高会根据卡片所在容器的宽度自动调整,不需要手动设置断点。</p>
<div class="card__meta">
<span class="card__tag">容器查询</span>
<span class="card__date">2025年3月</span>
</div>
</div>
</article>
</div>
</section>
<!-- 中等容器 —— 模拟侧边栏 -->
<section class="container-demo container-medium">
<h2>中等容器(400px ~ 700px)</h2>
<div class="card-wrapper">
<article class="card">
<!-- 卡片内容完全一样,省略 -->
</article>
</div>
</section>
<!-- 窄容器 —— 模拟移动端或更窄的侧边栏 -->
<section class="container-demo container-narrow">
<h2>窄容器(400px以下)</h2>
<div class="card-wrapper">
<article class="card">
<!-- 卡片内容完全一样,省略 -->
</article>
</div>
</section>
三个.card-wrapper都声明为查询容器,名字统一用card-container。它们唯一的区别是父级section设置了不同的max-width来模拟不同布局场景。真实项目中,这些宽度差异可能来自CSS Grid的列宽变化、Flex布局的伸缩,或者用户手动拖拽侧边栏。
3.2 容器声明与基础样式
先把三个演示容器的宽度定好,同时给.card-wrapper声明查询容器身份:
/* 三个演示区域的宽度设定 */
.container-wide {
max-width: 900px;
margin: 0 auto;
}
.container-medium {
max-width: 550px;
margin: 0 auto;
}
.container-narrow {
max-width: 340px;
margin: 0 auto;
}
/* 核心:把card-wrapper声明为查询容器 */
.card-wrapper {
container: card-container / inline-size;
/* 容器本身的一些基础样式 */
border: 1px dashed #ccc;
border-radius: 8px;
padding: 16px;
background: #fafafa;
}
一个容易被忽略的细节:容器元素上如果设置了overflow: hidden或overflow: auto,在某些浏览器中可能会影响容器查询的尺寸计算。Chrome和Firefox的行为已经基本统一,但Safari在嵌套容器加overflow时偶尔会出现尺寸不更新的bug。建议不要把查询容器和overflow容器设成同一个元素,中间隔一层wrapper会更稳妥。
3.3 写三个断点的容器查询
根据卡片在三种容器宽度下的理想布局,设定两个断点:容器宽度小于400px时用竖版布局,400px到650px之间用紧凑横版,大于650px时用完整横版。所有样式都写在@container card-container规则里:
/* 需要先在外部定义好card这个元素的基础结构 */
.card {
display: flex;
gap: 12px;
/* 默认就用竖版,移动优先的思路 */
flex-direction: column;
}
.card__image img {
width: 100%;
height: auto;
display: block;
border-radius: 6px;
}
/* 容器宽度大于400px:图片和文字并排,图片固定宽度 */
@container card-container (min-width: 400px) {
.card {
flex-direction: row;
align-items: flex-start;
}
.card__image {
flex: 0 0 160px;
}
.card__body {
flex: 1;
min-width: 0;
}
}
/* 容器宽度大于650px:图片区域更大,文字区域分成两列展示元信息 */
@container card-container (min-width: 650px) {
.card {
gap: 20px;
}
.card__image {
flex: 0 0 220px;
}
.card__meta {
display: flex;
justify-content: space-between;
align-items: center;
}
}
把这三个断点放进页面后,神奇的事情发生了:同一张卡片在宽容器里图片占220px、元信息左右分列;在中等容器里图片缩到160px、元信息还是上下堆叠但整体是横排;在窄容器里完全变成竖版,图片撑满卡片宽度。所有切换都是自动的,卡片被拖到哪个容器里就适配哪个容器的规则。
3.4 用容器相对单位微调字体和间距
断点切换解决了布局结构的问题,但还有一个细节可以打磨——在400px到650px的过渡区间里,字体和间距如果能平滑缩放,视觉上会更舒服。这时候容器相对单位就派上用场了:
/* 标题字号在容器宽度的5%到2rem之间平滑变化,最小不小于1rem */
.card__title {
font-size: clamp(1rem, 5cqi, 2rem);
line-height: 1.3;
margin-bottom: 0.5em;
}
/* 描述文字的字号也跟随容器宽度微调 */
.card__desc {
font-size: clamp(0.85rem, 2.5cqi, 1.1rem);
line-height: 1.6;
color: #555;
}
/* 标签的内边距用容器宽度单位,窄容器里标签自动变小 */
.card__tag {
display: inline-block;
padding: clamp(2px, 0.5cqi, 6px) clamp(6px, 1.5cqi, 12px);
background: #e8f0fe;
color: #1a73e8;
border-radius: 4px;
font-size: clamp(0.7rem, 1.8cqi, 0.9rem);
}
clamp()函数和容器单位的组合,让组件在任意宽度下都能保持协调的比例关系。以前要实现这个效果,要么写一堆媒体查询断点做阶梯式调整,要么用vw单位但那样组件脱离上下文后比例会失调。容器单位从根本上解决了”组件不知道自己在多大空间里”的问题。
四、处理嵌套容器与命名冲突
实际项目中容器很少是扁平的。一个仪表盘页面里,外层是Grid布局的主容器,内层每个Grid单元格里又有自己的卡片容器,卡片内部可能还有更小的功能区块需要响应式调整。多层容器嵌套时,@container默认会匹配最近的祖先容器,如果最近的容器尺寸不满足条件,它会继续向上查找,直到找到一个满足条件的容器或者抵达根元素。
这个默认行为在大多数情况下符合直觉,但当你需要精确控制”只响应某一层容器”时就显得不够用了。假设卡片内部有一个评论区组件,它只想根据卡片容器(名叫card-container)的宽度来调整布局,而不想受到更外层Grid容器的影响。这时候需要在@container规则里显式指定容器名称:
/* 只监听名为card-container的容器,忽略中间其他匿名容器 */
@container card-container (min-width: 500px) {
.comment-thread {
display: grid;
grid-template-columns: 40px 1fr;
}
}
命名容器让组件在嵌套层级中具备了”选择监听对象”的能力,这是匿名容器做不到的。建议项目里所有作为查询目标的容器都给一个有意义的名字,哪怕暂时只在单一层级使用,将来嵌套加深时也能避免意外匹配。
五、浏览器兼容性现状与渐进增强策略
截至2025年初,容器查询的核心语法在所有现代浏览器中已经得到支持:Chrome 105+、Firefox 110+、Safari 16+、Edge 105+。全球覆盖率超过92%,对于面向一般用户的Web项目来说,已经可以放心使用了。
但企业级项目通常需要覆盖更老的浏览器,或者处于过渡期的混合环境。这时候可以采取渐进增强的策略——把容器查询当作增强层,基础样式写在@container外面作为fallback:
/* 基础样式:所有浏览器都生效的竖版布局 */
.card {
display: flex;
flex-direction: column;
}
/* 增强样式:支持容器查询的浏览器在宽容器中切换为横版 */
@container card-container (min-width: 500px) {
.card {
flex-direction: row;
}
}
/* 对于完全不支持容器查询的浏览器,可以用@supports做一层判断 */
@supports not (container-type: inline-size) {
/* 降级方案:用传统的媒体查询或固定布局 */
@media (min-width: 768px) {
.card {
flex-direction: row;
}
}
}
@supports查询在这里起到了安全网的作用。支持容器查询的浏览器走上面@container的路径,不支持的走@media的降级路径。两者互不干扰,等旧浏览器淘汰后直接删掉@supports块就行,不影响核心逻辑。
六、一个更接近生产环境的例子:仪表盘布局
光讲卡片还不够,来看看容器查询在复杂的仪表盘布局中能发挥什么作用。假设页面结构是这样的:顶部是固定宽度的全局导航,左侧是可拖拽宽度的侧边栏,右侧是主内容区,主内容区里用CSS Grid放置了多个小组件(图表、列表、统计卡片等)。
<div class="dashboard">
<aside class="sidebar" style="width: var(--sidebar-width, 280px);">
<!-- 侧边栏内容 -->
</aside>
<main class="main-content">
<div class="widget-grid">
<div class="widget-wrapper">
<div class="widget">图表组件</div>
</div>
<div class="widget-wrapper">
<div class="widget">列表组件</div>
</div>
<div class="widget-wrapper">
<div class="widget">统计卡片</div>
</div>
</div>
</main>
</div>
每个.widget-wrapper声明为查询容器,.widget内部就可以根据.widget-wrapper的实际宽度来调整图表类型、列表密度、卡片排列方向。用户拖拽侧边栏改变--sidebar-width变量时,Grid会重新计算列宽,每个.widget-wrapper的宽度跟着变化,里面的组件自动响应——整个过程不需要JavaScript监听resize事件,也不需要手动给组件传新的props。
这个场景特别能体现容器查询相比ResizeObserver的优势。用ResizeObserver当然也能实现类似效果,但你需要维护观察者实例、在回调里更新组件状态、处理组件卸载时的清理逻辑。容器查询把这些都内化成了CSS引擎的原生能力,性能更好,代码也更少。
七、样式查询(style())的探索与限制
聊完了尺寸查询,有必要提一下容器查询规范中的另一个实验性能力——样式查询。语法上它用style()函数来查询容器的CSS属性值,而不是尺寸:
/* 查询容器上是否设置了特定的CSS变量值 */
@container style(--theme: dark) {
.card {
background: #1e1e1e;
color: #e0e0e0;
}
}
这个功能在概念上很诱人——组件可以根据容器上定义的CSS变量自动切换主题、密度模式、交互状态。但现实是,样式查询的浏览器支持目前还非常有限。Chrome从111版本开始在实验性标志后支持,Firefox和Safari尚未跟进。规范本身也还在演变中,查询哪些属性、性能开销如何控制等细节都没有最终敲定。
现阶段不建议在生产环境依赖样式查询。如果需要在组件间传递状态,更稳妥的做法还是用CSS变量配合尺寸查询做一个”模拟”:在不同尺寸断点下定义不同的变量值,让组件通过继承的变量来切换样式。
八、从这次实践中沉淀的几条原则
经过卡片组件和仪表盘两个案例的实战,我对容器查询的使用方式有了一些自己的总结。这些原则不一定适用所有项目,但至少在我参与的几个后台系统和内容站点里得到了验证。
第一,容器声明要放在”布局容器”上,而不是”内容容器”上。什么意思呢?.card-wrapper是布局容器,它决定了卡片在页面中占据多大空间;.card是内容容器,它只是被撑开的那个。查询容器应该设在布局容器上,这样卡片内部才能感知到外部给予的空间约束。把container-type直接写在.card上反而没用,因为.card的宽度是由其内容决定的,不会触发有意义的变化。
第二,断点值用相对单位比绝对像素更稳健。比如用em或rem写容器查询的断点,在用户调整浏览器默认字号时,断点也会跟着缩放。对于以文字内容为主的组件,这个细节对无障碍访问很重要。
第三,容器查询不能替代所有媒体查询。页面级的布局切换(比如导航栏从横排变成汉堡菜单)仍然应该用@media,因为这些决策跟视口宽度相关,跟某个具体组件所在的容器宽度无关。容器查询擅长的是”组件内部的响应式”,媒体查询负责的是”页面整体的响应式”,两者各司其职。
九、小结
容器查询带给前端的不仅是几个新属性和新规则,更是一种思维方式的松绑。以前写组件CSS的时候,脑子里得同时装着组件的内部结构和页面的大局布局,生怕组件在某个边角位置撑破或者挤扁。现在可以把组件的自适应逻辑完全封装在组件内部,写@container就像给组件内置了一套独立的响应式神经系统——放到哪儿都能自己调整,不用依赖父级传参,不用写监听脚本。
如果你的项目正在从旧版的全局CSS向组件化架构迁移,容器查询是一个很好的切入点。它和CSS Modules、Shadow DOM、以及各种CSS-in-JS方案都能共存,不要求你改变现有的构建流程。从最复用的那几个组件开始改起,把那些靠.is-compact修饰类勉强维持的响应式逻辑,逐步迁移到@container里。改完之后你会发现,组件的CSS文件里少了一堆条件判断,HTML里少了一堆动态类名,代码清爽了不少。
容器查询的浏览器支持已经到了可以应用于生产环境的阶段,配套的开发工具(Chrome DevTools对容器查询的检查和调试支持)也已经比较完善。如果你还没有在实际项目中用过它,不妨拿一个非核心页面的小组件试试水,感受一下”组件知道自己有多大”这种全新的写法带来的便利。

