做前端的这些年,响应式布局基本离不开媒体查询。以前项目里全是 @media (max-width: 768px) 这种代码,写多了真的有点烦。特别是组件复用的时候,同一个卡片在不同容器里要表现不同尺寸,媒体查询根本管不到那块。
还好,容器查询(CSS Container Queries)终于来了,这玩意儿才是最接近「组件级响应式」的方案。这篇文章不聊虚的,直接拿一个真实的卡片组件来演示。
一、容器查询能解决什么问题
传统的媒体查询是看浏览器窗口有多大,但组件往往是被放在某个容器里的。窗口大了,不代表容器就大了;窗口小了,容器可能还是很宽。这就导致了一个问题:组件无法根据自己实际所在的容器尺寸来调整布局。
容器查询的思路很简单:给容器定义一个类型,然后用 @container 来查询容器的大小。
基本语法:
/* 定义容器 */
.card-wrapper {
container-type: inline-size;
}
/* 查询容器宽度 */
@container (min-width: 400px) {
.card {
display: flex;
}
}
这里 container-type: inline-size 的意思是说,只跟踪容器的内联方向宽度(对于水平书写的页面来说,就是宽度)。还有 container-type: size 可以同时跟踪宽和高,但要注意,设置了 size 之后,容器自身的尺寸就不能由内容决定了,所以一般用 inline-size 就够用了。
二、动手写个卡片组件
这个卡片组件想要的效果是:
- 容器宽度小于 400px 时,卡片上下排列,图片在上,文字在下。
- 容器宽度大于等于 400px 时,卡片水平排列,图片在左,文字在右。
HTML 结构:
<div class="grid">
<div class="card-container">
<article class="card">
<img src="cover.jpg" alt="封面图">
<div class="card__content">
<h3>卡片标题</h3>
<p>这是一段描述文字,用来演示容器查询的效果。</p>
<a href="#" rel="external nofollow" >查看详情</a>
</div>
</article>
</div>
</div>
CSS 可以这样写:
.card-container {
container-type: inline-size;
}
.card {
display: grid;
gap: 16px;
}
@container (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
align-items: center;
}
.card img {
width: 200px;
height: 100%;
object-fit: cover;
}
}
这样,每个卡片组件的布局就完全由它所在容器的宽度决定,跟浏览器窗口大小没关系了。比如你把同一个卡片放在侧边栏,它就是上下布局;放在主内容区,它就是左右布局。这才是真正意义上的组件级响应式。
三、把容器查询和 :has() 结合起来用
如果说容器查询解决的是「组件根据容器尺寸自适应」,那 :has() 选择器解决的就是「根据子元素状态来给父元素样式」。这两个配在一起,非常好用。
举个例子,还是那个卡片组件,如果卡片里的图片加载失败了,或者干脆没有图片,我希望卡片把标题区域放大到全宽。用 :has() 可以这样:
.card:has(img) .card__content {
padding: 16px;
}
.card:not(:has(img)) .card__content {
padding: 24px;
font-size: larger;
}
这两行代码的含义很直白:如果有 img 元素,就按照正常的 padding 来;如果没有 img,就把内容区的 padding 变大,字号也变大一点。真的,:has() 让我写 CSS 的逻辑感强了很多。
:has() 在 2023 年底就已经被所有主流浏览器支持了,不需要任何前缀,放心用。
四、CSS 嵌套也别落下了
CSS 嵌套是 Sass、Less 早已支持的特性,现在原生 CSS 也支持了。嵌套的好处就是层级关系清晰,不用反复写父选择器。
容器查询加上 CSS 嵌套,写起来就非常舒服:
.card-container {
container-type: inline-size;
.card {
display: grid;
gap: 16px;
img {
border-radius: 12px;
object-fit: cover;
width: 100%;
height: 120px;
}
@container (min-width: 400px) {
grid-template-columns: 200px 1fr;
img {
height: 100%;
border-radius: 12px 0 0 12px;
}
}
}
}
原来要在 @media 里外来回切换,现在直接嵌套着写,代码可读性直接拉满。这个在 Chrome 112+、Safari 17+ 都原生支持了,真的是等了很多年才等到的能力。
五、实际使用中的几个坑
任何新特性刚上手都会踩坑,容器查询也不太一样,我列几个真实的坑:
1. container-type 不能乱设
container-type: inline-size 的时候,容器本身不能是 inline 元素。也就是说,如果你给一个 span 设置了 container-type: inline-size,它是不会生效的,因为 span 默认就是 inline。得先把它改成 block 或 inline-block 才行。
2. 注意子元素对容器的影响
容器的尺寸如果被子元素撑开,而子元素又依赖容器查询来布局,这就形成了一个循环依赖。浏览器会忽略这种情况,所以别这么干。解决办法是,让容器的宽度由外部布局决定,而不是由内部内容撑开。
3. 浏览器兼容性
容器查询从 Chrome 105 开始支持,Safari 16 开始支持,Firefox 110 开始支持。2024 年基本所有浏览器都能用了。如果项目还有极老的浏览器要兼容,那就只能考虑 @supports 加 fallback 了。
4. 别啥都用容器查询
容器查询适合的是那些在多个地方复用的组件。如果是全局布局层面的响应式,比如侧边栏收起、顶部导航切换成汉堡菜单这种,媒体查询仍然是更合适的选择。没必要为了用新技术而用新技术。
六、一个封装小技巧
最后给大家一个思路,可以封装一个工具类来开启容器查询,这样不用每次都写 container-type:
.container {
container-type: inline-size;
}
加一个类名就完事,其他地方直接写 @container 就行。当然也可以给容器命名,像下面这样:
.card-container {
container: card / inline-size;
}
@container card (min-width: 400px) {
/* 只影响名字为 card 的容器 */
}
命名之后的好处是,页面里有很多个容器的时候,不会互相干扰,查询条件会更明确。
最后想说的
这篇文章主要就是想聊一下容器查询在实际项目里怎么用。以前总觉得这是个新特性,离生产环境很远。但实测下来,配合 :has() 和 CSS 嵌套,写组件真是太舒服了。如果你还没试过,我建议找一个简单的组件开始重构一下,感受会非常直观。
CSS 这两年的变化真的很大,从 :has()、容器查询到原生嵌套,每一次更新都在把之前在 CSS 预处理器里才能做到的事情慢慢变成原生能力。虽然落后了 Sass 很多年,但总算落地了。
如果你也在自己的项目里用上了容器查询,或者踩了什么坑,欢迎来跟我聊聊。

