在写组件的时候,你有没有遇到过这种情况?同一个卡片组件,放在侧边栏里和放在主内容区里,明明都是100%宽度,但它看起来却完全不是一个感觉。你只能靠媒体查询猜一个大概的视口宽度,然后去调整它。可问题是,组件的实际容器宽度跟视口根本不成正比。以前这是个大难题,现在CSS容器查询(Container Queries)终于能解决了。
这篇文章我不会跟你扯太多概念,直接用一个真实的案例带你上手,顺便聊聊容器查询带来的设计思路转变。
先看以前咋写自适应组件
以前你想让一个产品卡片在窄容器里显示为上下结构,在宽容器里显示为左右结构,你大概率会写:
@media (min-width: 600px) {
.card {
flex-direction: row;
}
}
这个意思是视口大于600px的时候卡片横向排列。但是你把这张卡片放进了侧边栏,整个侧边栏只有300px宽,视口就算是1200px也没用,卡片里的内容照样被挤成一坨。反过来,你把卡片放在一个宽屏区域内,但实际上你希望它横向排列,可视口不够宽,它又变回上下结构了。
核心问题在于:视口宽度 ≠ 元素父容器宽度。容器查询就是专门来解决这个尴尬事的。
容器查询到底咋用
用起来其实很简单,分两步。第一步,给你的父容器设置一个 container-type,让它变成一个可以查询的“容器”。第二步,用 @container 来写适应规则。
举个最基础的例子。假设我有一个 .content 容器,里面有一张卡片。我希望容器的宽度小于400px时,卡片显示为上下排列;大于等于400px时,卡片变成左右排列。代码如下:
<div class="content">
<div class="card">
<img src="thumb.jpg" alt="产品图">
<div class="info">
<h3>商品标题</h3>
<p>商品描述文字,可能比较长。</p>
<button>立即购买</button>
</div>
</div>
</div>
.content {
container-type: inline-size;
/* 或者 container: inline-size; */
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
看明白没?@container 查询的是 .content 这个容器自身的宽度,跟视口半毛钱关系都没有。只要这个容器宽度超过400px,里面的卡片就变横排,不管视口是手机还是电脑。
注意:container-type: inline-size 表示我只看水平方向的尺寸。如果你也想查询高度,可以写成 block-size,但通常我们只需要水平方向,而且同时查询宽高很容易产生循环依赖,所以尽量只用 inline-size。
给容器起个名字
一个页面上可能有几十个容器,你肯定不想所有的 @container 都混在一起。所以最好给容器命名,查询时指定名字。
例如:
.content {
container-name: content-area;
container-type: inline-size;
}
@container content-area (min-width: 600px) {
.card {
padding: 20px;
}
}
这样你就能很精确地告诉浏览器:“这个规则只作用于 content-area 这个容器内部的元素”。如果容器没命名,那么最近的拥有 container-type 的祖先容器就会被当作查询目标。
真正的案例:侧边栏和主内容区共用的卡片
光说没用,我们弄一个实际页面看看效果。假设页面左边是固定宽度的侧边栏,右边是主内容区。同样的产品卡片,在侧边栏要显示得紧凑一点,在主内容区要舒展一点。
HTML结构大概这样:
<div class="page">
<aside class="sidebar">
<div class="product-card">
<img src="headphone.png" alt="耳机">
<div class="product-info">
<h3>无线降噪耳机</h3>
<p>拥有40小时续航,自适应降噪系统</p>
<span class="price">¥399</span>
<button>加入购物车</button>
</div>
</div>
</aside>
<main class="main-content">
<div class="product-card">
<img src="headphone.png" alt="耳机">
<div class="product-info">
<h3>无线降噪耳机</h3>
<p>拥有40小时续航,自适应降噪系统</p>
<span class="price">¥399</span>
<button>加入购物车</button>
</div>
</div>
</main>
</div>
给侧边栏和主内容区都设置为容器,而且都起不同的名字:
.sidebar {
width: 300px;
container-name: sidebar-area;
container-type: inline-size;
}
.main-content {
flex: 1;
container-name: main-area;
container-type: inline-size;
}
然后写两套 @container 规则,分别调整卡片内部布局。侧边栏宽度固定是300px,所以不用写查询条件,直接写默认样式。主内容区的宽度会变化,所以我们用查询条件让它自动适应:
/* 全站默认卡片样式(适用于侧边栏这种窄容器) */
.product-card {
display: flex;
flex-direction: column;
background: #fafafa;
border-radius: 12px;
padding: 12px;
}
.product-card img {
width: 100%;
border-radius: 8px;
}
.product-info {
padding: 10px 0 0;
}
.price {
color: #d4380d;
font-weight: bold;
}
/* 当容器宽度大于等于 600px 时,让卡片变成左右结构 */
@container main-area (min-width: 600px) {
.product-card {
flex-direction: row;
align-items: center;
padding: 20px;
}
.product-card img {
width: 40%;
margin-right: 20px;
}
.product-info {
padding: 0;
}
}
这样写完之后,主内容区里的卡片只要超过600px宽度就会自动横过来,而侧边栏始终保持紧凑竖排。你可能会问,如果主内容区宽度不到600px呢?那就跟侧边栏一样竖排,完全没问题。
如果用以前的媒体查询,你得先知道主内容区在什么视口下超过600px,然后写一个很主观的断点。而且如果页面布局变了(比如侧边栏改成可拖拽宽度),你的媒体查询可能就废了。而容器查询是实时根据容器尺寸变化的,这就是它最大的优势。
容器查询单位:cqw、cqh、cqi、cqb
除了 @container 规则,容器查询还带了一整套单位,用来计算容器尺寸。这个在写组件的时候特别有用。
1cqw= 容器宽度1%1cqh= 容器高度1%1cqi= 容器内联轴(通常水平)1%1cqb= 容器块轴(通常垂直)1%1cqmin= 取宽度和高度中较小的1%1cqmax= 取宽度和高度中较大的1%
其中 cqw 和 cqi 在布局中比较常用。比如我想让卡片图片的宽度始终是容器宽度的45%,就可以写:
.product-card img {
width: 45cqi;
}
这里的 cqi 就不用care视口或者父级有没有设置container-type了——它要求最近的容器有container-type才行。如果没有容器,它就等于一个小尺寸的视口单位?实际上如果没有容器,这些单位会回退到视口单位(vw、vh等)。所以用起来要小心一点,确保元素在容器内部。
结合容器查询,可以做出很细腻的自适应组件。比如一个列表项,我希望图标大小跟容器宽度正相关,但又不想写死px值。以前你得用媒体查询覆盖好几档,现在直接写:
.icon {
width: 10cqi;
height: 10cqi;
}
容器窄图标就小,容器宽图标就大,完全线性变化。
跟 :has() 搭配还能玩点花活
现在很多现代浏览器也支持了 :has() 选择器。你可以用它来检测一个容器里面有没有某个元素,从而影响容器的样式。比如一个容器里有张图片和没有图片,组件的间距完全不同。
.media-card {
container-type: inline-size;
padding: 10px;
}
/* 容器内有图片时,增加padding */
.media-card:has(img) {
padding: 20px;
}
/* 容器宽度大于500px,并且有图片时,让图片和文字并排 */
@container (min-width: 500px) {
.media-card:has(img) {
display: flex;
}
}
这种写法能根据内部内容反过来调整自身布局,配合容器查询,几乎可以把组件做到“自我适应”的极致。不过 :has() 在容器查询里使用的时候,注意别让选择器产生循环依赖,比如查询容器自身是否包含某个子元素时就设置该元素的样式,那可能导致样式一直无法确定。
实际项目中需要注意的坑
1. 容器查询会失效的场景
如果父容器的尺寸依赖于子元素,那子元素再去查询父容器就会形成循环。所以一般给容器设置 container-type: inline-size 时,它不能是inline元素,最好是一个块级或flex/grid容器。另外,如果容器内部有内容撑开它的宽度,这时你用容器查询去改内容宽度就可能很奇怪,因为内容变宽会导致容器变宽,宽度又变,可能产生抖动。解决办法是不要让内容直接决定容器宽度,容器宽度应该由它的父级约束(比如百分比宽度或flex分配)。
2. 浏览器兼容性
目前容器查询在最新的Chrome、Edge、Safari和Firefox里都已经支持了。但如果你要兼容3年前的浏览器,那还得用媒体查询兜底。我的做法是:先写一套基于媒体查询的基本样式,然后加上容器查询进行增强。因为容器查询的代码如果不支持,浏览器会忽略,不影响基本布局。这种做法虽然有点保守,但很稳妥。
3. 别把所有东西都丢进去
容器查询确实好用,但也不是所有场景都需要。如果一个组件只在全屏页面里展示,或者你的页面布局很固定,那直接媒体查询更简单。容器查询更适合那些跨多区域复用的组件,比如商品卡片、文章条目、信息面板等。
写在最后
容器查询让我觉得CSS终于开始真正想清楚了组件的“容器”到底是什么网络环境。以前做组件自适应,总是要猜屏幕宽度,然后调整组件内部的布局,这本质上是在用错误的角度做正确的事。现在容器查询能让你从组件的角度去思考,界面里的每个组件都能根据自己所在的“盒子”的大小来表现,确实省心不少。
最后给你留个小作业:试着把一个导航菜单写成容器查询组件,让它在宽容器里显示水平菜单,在窄容器里变成一个汉堡按钮。写出来你就掌握了容器查询的核心逻辑。不过注意,汉堡按钮的点击交互还得靠JavaScript,但样式的切换用容器查询就能搞定。
好了,这篇文章是实打实的经验分享,没有那些花里胡哨的东西。希望对你有用。

