一、我们为什么需要容器查询
过去这十几年,前端做响应式布局基本就是围着媒体查询打转。屏幕宽度小于某个值,布局从横变竖;大于某个值,侧边栏展开。这套逻辑在整页级别的调整上一直很好用,可问题在于——它只能感知视口的大小,完全不知道组件自己处在什么样的容器里。
举一个再常见不过的例子:一个商品卡片组件,有时候它被放在一个很宽的主内容区里,有时候又被塞进一个只有三百多像素宽的侧边栏里。用传统的媒体查询,你没法只靠那个卡片自己的处境来改变样式,因为媒体查询看的是整个窗口的宽度,不是卡片容器的宽度。最后的解决方式往往是给卡片加一堆父级相关的class,或者用grid和flex的自动换行去硬撑。代码写得很别扭,维护起来也麻烦。
容器查询的出现,就是要把这个逻辑正过来。它允许你基于一个容器元素的尺寸,而不是整个视口的尺寸,来决定内部元素的样式。换句话说,你可以在CSS里直接表达“如果我的父容器宽度大于400px,我就显示成两列的布局;如果父容器宽度小于400px,我就堆叠成单列”。这种能力让组件真正做到了“自包含的响应式”,放到任何地方都能自适应。
二、容器查询的核心语法
容器查询涉及两个关键步骤:第一,给父容器定义一个“包含上下文”;第二,在内部元素上用@container规则编写条件样式。
1. 声明容器
要在某个元素上启用容器查询,需要设置container-type属性。常用的值有三个:
inline-size:只根据容器的内联方向尺寸(水平书写模式下就是宽度)进行查询。size:同时监听容器的宽度和高度。normal:默认值,不创建容器上下文。
你也可以给容器起个名字,用container-name,这样在@container里可以指定具体查询哪个容器,避免嵌套时的冲突。
2. 编写查询条件
@container的语法和@media非常接近,但后面跟的是容器名称(可选)和条件。比如:
@container (min-width: 400px) {
.card {
grid-template-columns: 1fr 1fr;
}
}
如果给容器命名了,就写成@container my-container (min-width: 400px)。没有命名的话,@container会匹配离自己最近的一个祖先容器。
三、上手案例:自适应卡片的完整实现
假设我们有一个卡片组件.card,里面包含一张图片和一段介绍文字。当卡片的容器宽度超过500px时,图片和文字左右并排;当容器宽度不足500px时,图片在上,文字在下。不管是放在宽栏还是窄栏,卡片都能自动切换布局。
HTML结构很简单:
<div class="card-container">
<div class="card">
<img src="product.jpg" alt="商品图" class="card-img">
<div class="card-body">
<h3>产品名称</h3>
<p>一段简洁的产品描述文字。</p>
</div>
</div>
</div>
CSS部分,首先要把外层容器声明为一个容器查询上下文:
.card-container {
container-type: inline-size;
container-name: card-container;
}
接着定义卡片的基础样式(移动优先,默认是竖版布局),然后用容器查询来覆盖宽容器下的样式:
.card {
display: grid;
grid-template-columns: 1fr; /* 默认单列 */
gap: 16px;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.card-img {
width: 100%;
height: auto;
border-radius: 4px;
}
/* 容器宽度超过500px时,切换为两列布局 */
@container card-container (min-width: 500px) {
.card {
grid-template-columns: 1fr 1fr;
align-items: center;
}
}
这套代码写完,卡片的响应式行为就完全和视口解耦了。你把.card-container丢到任何地方——无论是一个弹性网格的一个单元格,还是一个固定宽度的侧边栏,卡片都会根据这个容器的实际宽度自主调整内部排版。这种体验是用传统媒体查询很难实现的。
四、进阶玩法:样式查询
容器查询除了基于尺寸,还有一个叫“样式查询”的变体,目前还处于比较新的阶段(Chrome 111+支持)。样式查询可以基于容器的自定义属性(CSS变量)的值来决定内部样式。这相当于让CSS有能力根据某个开关状态切换整个子树的样式。
比如我们经常遇到深色模式的需求,传统做法是整个页面在html或body上加一个class,然后所有组件用这个class来覆盖颜色。样式查询提供了一种更干净的方式:在容器上定义一个自定义属性--theme: dark,然后子元素用@container style()来响应。
首先,给容器设置自定义属性:
.card-container {
--theme: light;
container-name: card-container;
}
.card-container.dark {
--theme: dark;
}
然后在CSS中查询这个变量的值:
@container card-container style(--theme: dark) {
.card {
background-color: #1e1e1e;
color: #ffffff;
}
.card-img {
filter: brightness(0.8);
}
}
这样做的好处是,你不需要在每一层组件里都去检查根元素的类名,样式查询直接作用于包含块的局部范围内。对于设计系统里的组件库来说,这意味着组件可以感知父级提供的主题变量,做出相应的样式调整,而不需要依赖全局上下文。这在大型应用里会大幅降低样式管理的复杂度。
五、实战组合:仪表盘卡片系统
为了更完整地展示容器查询和样式查询的配合,我们做一个稍微复杂点的例子:一个仪表盘界面,里面可以动态添加卡片,卡片容器可能是两栏、三栏或者通栏,并且整个仪表盘支持一键切换暗色模式。
1. 容器尺寸查询自动调整卡片内图表和数字的排版
每个卡片内部展示一个标题、一个数字和一个简单的柱状图(用几个div模拟)。容器宽度大于350px时,数字和柱状图并排;小于350px时,数字在上,柱状图在下。代码和之前卡片类似,只是多加了一个图表元素。
2. 样式查询切换暗色模式
在仪表盘的最外层容器上加一个自定义属性--color-scheme,然后用样式查询来决定卡片内部的背景、文字颜色和图表颜色。
.dashboard {
--color-scheme: light;
container-name: dashboard;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.dashboard.dark {
--color-scheme: dark;
}
/* 卡片内部通过样式查询响应 */
@container dashboard style(--color-scheme: dark) {
.card {
background: #2c2c2c;
color: #f0f0f0;
}
.bar {
background: #4a90e2;
}
}
切换模式的按钮只需要在最外层容器上增删.dark类,所有卡片自动完成颜色过渡。不用手动去改每一个组件。
六、浏览器兼容性与渐进增强策略
容器查询和样式查询在现代浏览器里的支持已经相当不错。Chrome、Edge从105版本开始完整支持@container尺寸查询,从111版本开始支持样式查询。Firefox在110版本也跟上了步伐。Safari 16开始支持尺寸查询,但样式查询还在预览阶段。
对于需要兼容老旧浏览器的项目,可以采用渐进增强的思路:先按照传统的移动优先方式写一套基础的竖版样式,保证在不支持容器查询的浏览器里能用。然后用@supports检测浏览器是否支持容器查询,在支持的环境里启用增强的响应式行为。
/* 基础样式:全部竖版 */
.card {
display: block;
}
/* 检测容器查询支持 */
@supports (container-type: inline-size) {
.card-container {
container-type: inline-size;
}
/* 然后在 @container 里写增强样式 */
@container (min-width: 500px) {
.card {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
}
这样,旧浏览器看到的始终是基础的单列布局,现代浏览器则能体验到根据容器宽度变化的动态排版。用户体验不会因为技术迭代而受损,这是一个很平滑的过渡方式。
七、使用中容易踩到的几个坑
1. 容器不能是内联元素
如果一个元素的display是inline,你给它设置container-type不会生效。容器查询要求容器是块级或者具备格式化上下文的元素(如flex容器、grid容器)。最简单的处理就是确保容器是display: block、flex或grid。
2. 容器查询不能查询自身
你不能在一个元素上既定义container-type又在自己的@container里查询自己。容器查询的作用范围是容器的后代元素,而不是容器自身。如果需要根据自身尺寸改变某个元素自己的样式,那还是得借助传统的媒体查询或JavaScript。
3. 样式查询的性能开销
样式查询的引入让浏览器需要在每次CSS变量改变时重新评估相关规则,理论上比纯尺寸查询多了一些开销。但现代引擎的优化已经做得很好,只要不滥用(比如在高频动画中频繁改变查询所依赖的自定义属性),几乎感觉不到性能差异。养成好习惯,只在必要时使用样式查询即可。
4. 命名容器的冲突
当多个具名容器嵌套时,@container card-container会从内向外查找第一个匹配的容器。如果你在内层也定义了一个同名容器,可能会匹配到非预期的那个。一般建议给容器起有意义的名字,并且避免在嵌套层级中使用重复的名字。如果不命名,@container会选择最近的祖先容器,在多数场景下反而更安全。
八、容器查询带给我们的思维转变
容器查询不只是多了一个CSS语法,它实际上在推动一种“组件优先”的响应式设计思路。过去我们做页面,总是下意识地先想“在手机上是这样,在桌面端是那样”,所有断点都基于设备宽度。现在我们可以反过来思考:先设计组件在各种容器宽度下的表现,然后把它们像乐高一样拼进任意尺寸的布局中。组件的适应性和可移植性会因此大幅提升。
结合CSS Grid的auto-fit和minmax,再加上容器查询,我们甚至可以做出完全自适应的栅格系统:列数由视口决定,而每一列内部的组件再根据列的当前宽度调整内部样式。这种两层响应式机制,才是真正贴合现代web布局需求的答案。
前端一直在进步,容器查询和样式查询的落地,意味着那些曾经需要用JavaScript监听容器尺寸变化来实现的hack(比如ResizeObserver加动态class)可以正式退场了。这件事本身就是一种很实在的快乐——用更少的代码,实现更自然的响应式。
下次再碰到那种需要在不同地方展示同一个组件,但又要保持优雅排版的需求,不妨直接把容器查询用起来。你会发现,页面上的那些小卡片、小面板,突然就变得聪明了。

