以前提到响应式,第一反应就是用 @media 根据视口宽度来改样式。问题是,视口宽度跟组件宽度有必然关系吗?没有。一个侧边栏里的卡片,和一个弹窗里的卡片,就算视口一样宽,它们的实际可用空间可能差了十万八千里。这次趁着容器查询(Container Queries)和 :has() 都稳定了,我把项目里的卡片组件完全重写了一遍,代码量减了不少,而且每个组件都特别“聪明”——它只依据自己所在容器的宽度来调整布局。今天就用这个实际案例讲讲怎么用。
一、容器查询到底解决了什么问题?
举个很常见的场景:页面左边是窄侧边栏,右边是主内容区。同一个卡片组件,放到侧边栏里,我们希望它变成简单的上下结构:图片在上,文字在下;放到主内容区里,我们希望它变成左右结构:图片在左,内容在右。用 @media 实现的话,你会拿视口宽度去猜组件的宽度,比如“视口大于1200px就左右布局”,但侧边栏里那个卡片即使视口很大,它自己还是窄的。真要精确判断,你只能给每个区域定义一堆工具类或者用 CSS Grid 配合 auto-fit/minmax 去碰运气。容器查询就干净了:组件只看自己父级容器的宽度,达到一定阈值就换布局。
二、:has() 为什么也很重要
:has() 选择器能让你基于子元素的状态去“回溯”应用样式。举个例:一个 .card 内部有一个 .card__thumbnail,当缩略图存在的时候,卡片可能要有额外的内边距;或者当卡片里有一个“VIP标识”时,边框要变成金色。以前这种需求,你得在 JS 里判断加 class。现在直接写 CSS:.card:has(.card__badge) { border-color: gold; }。简洁,而且样式跟逻辑分离。
更关键的是,容器查询有时候也要配合 :has() 才真正顺手。比如我想让容器查询根据内部是否包含某个元素来调整容器本身的尺寸行为,或者根据数量来控制排列,有了 :has() 就方便了。
三、开始实战:重构一个自适应卡片组件
1. 先写基础HTML结构
我们的卡片结构长这样:
<div class="container">
<article class="card">
<div class="card__media">
<img src="photo.jpg" alt="缩略图">
</div>
<div class="card__body">
<h3>产品名称</h3>
<p>这是产品描述,这里写一大段文字,模拟真实内容。</p>
<span class="card__badge">热卖</span>
<button>查看详情</button>
</div>
</article>
</div>
容器是 .container,卡片是 .card。接下来我们要让 .card 根据 .container 的宽度来改变布局。
2. 给容器开启容器查询
在父元素上设置 container-type: inline-size。这个属性的意思是:这个容器作为查询参考,我们只关心它的内联轴(水平方向)宽度。
.container {
container-type: inline-size;
container-name: card-container;
}
你甚至可以给容器起名,方便在多个嵌套容器中指定查询哪一个。这里我叫它 card-container。
3. 用 @container 写不同的布局
默认情况下(布局较窄),卡片上下排。
.card {
display: block;
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.card__media img {
width: 100%;
height: auto;
border-radius: 4px;
}
.card__badge {
display: inline-block;
background: gold;
color: #333;
font-size: 12px;
padding: 2px 8px;
border-radius: 20px;
margin-top: 8px;
}
当容器宽度大于等于400px时,我希望图片在左,内容在右。
@container card-container (min-width: 400px) {
.card {
display: flex;
gap: 16px;
}
.card__media {
flex: 0 0 100px;
}
.card__body {
flex: 1;
}
}
就这么简单。没有考虑视口,只关心卡片实际所在的区域。
4. 用 :has() 状态修饰
现在我想:如果卡片里有不好的评论标签,比如“促销中”,我们可能希望卡片边框变成橙色。但我不想在JS里加类。直接用 :has() 选中带有该标签的卡片,然后修改样式。
.card:has(.card__badge--promo) {
border: 1px solid orange;
}
还可以更厉害:如果卡片描述文字超过一定数量(粗略用子元素数量或者是否含有特定元素来判断),我们甚至可以调整布局。比如当卡片里带有两个按钮时,让按钮堆叠排列。
.card:has(.card__action + .card__action) {
flex-direction: column;
}
当然,你需要在HTML里有点对应的结构。
5. 把容器查询和 :has() 组合起来,写一个真实场景
假设卡片有两种状态:默认和“VIP专属”。VIP卡片会包含一个皇冠图标徽章。当卡片处于较宽容器时,我不仅希望左右排布,还希望VIP卡片有更高的媒体区占比、更奢华的边框。
@container card-container (min-width: 600px) {
.card:has(.card__badge--vip) {
border: 2px solid darkgoldenrod;
box-shadow: 0 4px 12px rgba(184, 134, 11, 0.3);
}
.card:has(.card__badge--vip) .card__media {
flex-basis: 120px;
}
}
这样的写法,逻辑一目了然:容器够宽,并且有VIP徽章,卡片就特殊处理。
四、浏览器兼容性到底怎么样?
说实话,现在容器查询和 :has() 在最新版Chrome、Edge、Firefox、Safari都支持了,很多用户早已不在IE时代。你要是实在不放心,可以用 @supports 去做一个“优雅降级”:不支持容器查询时,用默认的上下布局,反正也不会乱。
@supports (container-type: inline-size) {
.container {
container-type: inline-size;
}
@container card-container (min-width: 400px) {
.card {
display: flex;
}
}
}
不支持的时候,卡片就是上下结构,看着也正常。这就是渐进增强。
五、实际项目中我踩过的几个坑
坑1:容器查询容易被父级元素的 display 影响
容器查询要求父级内容尺寸能够被计算。如果你给容器设置了 display: inline-block 或某些特殊布局,比如表格单元格,它的宽度计算可能会有问题。最好给容器设为块级元素或者自动宽度的flex子项。
坑2:子元素中不要用“恨死”的 position: fixed
如果你的卡片内部有绝对定位或固定定位的元素,容器查询会按照包含块进行限制,尽量少在卡片内部使用 position: fixed,它会跳出容器上下文,导致你可能找不到问题在哪。
坑3:把容器查询套在Grid布局里,要留意最小宽度的默认值
Grid 子项默认的最小宽度是 auto,会让子项根据内容撑开宽度,导致容器查询不生效。解决办法是设置 min-width: 0。
.container {
min-width: 0;
container-type: inline-size;
}
六、一个更大的实战:响应式列表自动切换
我想要一个产品列表,容器窄的时候列表展示单列,容器宽一点展示双列,再宽就三列。传统 @media 做不到这种“自身宽度”的适配,而容器查询可以轻松搞定。
<div class="list">
<div class="item">产品1</div>
<div class="item">产品2</div>
<div class="item">产品3</div>
</div>
.list {
container-type: inline-size;
container-name: list-container;
display: grid;
gap: 16px;
grid-template-columns: 1fr;
}
@container list-container (min-width: 400px) {
.list {
grid-template-columns: repeat(2, 1fr);
}
}
@container list-container (min-width: 700px) {
.list {
grid-template-columns: repeat(3, 1fr);
}
}
把容器查询放在父容器上,然后修改grid列数。这样这个列表无论塞到侧边栏、主内容区、还是抽奖弹窗里,它都能自动匹配环境。这才叫组件化。
七、再给个 :has() 的妙用:写一个星级评分组件
有的组件库里,星级评分需要根据鼠标值高亮前面的星星。通常需要JS控制类名。其实用 :has() 可以简化为“悬停在某颗星星上,选中之前所有星星”。不过这个是交互类,就不展开了。单纯说布局方面:当输入框处于聚焦状态且内部有图标时,给边框加阴影。
.search-field:has(input:focus) {
box-shadow: 0 0 0 3px rgba(21, 156, 228, 0.3);
}
这就避免了给父级写“focus-within”的老方法。当然现实中两个都行。
八、总结:这波改动到底值不值?
我刚把项目里所有卡片、列表、面板都改成基于容器查询和 :has() 的写法。最大的感受是,再也不需要为了一个组件在不同位置的显示效果去写一大堆“覆盖样式”或者“响应式断点”了。每个组件的样式自带了“空间感知”,变得特别独立。也许这就是未来前端CSS组件化的一种常态。
当然,也不用急着把全部代码重写。你可以挑一两个常用组件试试手,比如侧边栏的卡片,或者消息列表项。等真正体会到“看容器不看视口”的快乐时,就回不去了。总之,CSS正在越来越强,像容器查询、:has()这类特性,不是锦上添花,而是咱们偷懒的有力工具。
希望大家都能用起来,少加一些JS类名。

