一直在写响应式,大家默认的套路就是媒体查询,看屏幕宽度,然后一顿改元素样式。可是你有没有想过,一个卡片组件放在侧边栏和放在主内容区,它的可用宽度完全不同,但媒体查询根本管不了这事。
前端圈这两年最烦人的词就是“XXX已死,YYY替代”。但我今天要说的是,容器查询这玩意真应该早点来。加上同样新出的:has()和层叠层,写组件的思路直接换了一个维度。
这篇文章我不讲理论废话,直接上手重写一个真实场景下的卡片组件。读完整篇你至少能搞明白:容器查询怎么解决模块自适应,:has()怎么帮你省掉一堆类名切换,层叠层怎么让你再也不用怕样式覆盖。
一、先看老写法哪里别扭
过去咱们做卡片,通常是一套基础样式加一套响应式补丁。比如一个文章卡片:
.card {
display: flex;
flex-direction: column;
padding: 16px;
}
.card img {
width: 100%;
height: auto;
}
.card-content {
padding: 12px;
}
@media (max-width: 768px) {
.card {
flex-direction: row;
align-items: center;
}
}
问题显而易见:这个md宽度是屏幕宽度。可是卡片可能被放在一个300px的侧边栏里,也可能放在一个1000px的中间栏里。屏幕宽度一样,侧边栏里卡片撑得变形,中间栏里卡片又太窄显示不全。
所谓“容器查询”就是解决这个:让组件根据它的父容器宽度来调整自身样式,而不是根据视口。
二、容器查询的用法,和你想象的差不多
首先给卡片的父容器加上一个“容器标识”,意思是“我这个容器里的后代元素可以查询我的宽度”。
.card-wrapper {
container-type: inline-size;
container-name: card-container;
}
解释一下:container-type: inline-size表示只跟踪内联轴(横向)尺寸,性能比container-type: block-size好。container-name用来起名字,下面查询的时候要用。
然后容器内部的卡片,就能用@container代替@media来写响应式:
.card {
display: flex;
flex-direction: column;
}
@container card-container (min-width: 400px) {
.card {
flex-direction: row;
align-items: center;
}
.card img {
flex-basis: 40%;
}
}
意思是当父容器宽度超过400px时,卡片自动切换成横排。这跟媒体查询的写法几乎没区别,但作用对象从视口变成了父容器。
实际项目里,同一个卡片组件可以放在任何宽度容器中,不用根据页面上不同位置去写各种覆盖样式,整个组件自成一套体系。这是很爽的一件事。
三、:has() 让状态级联不用再加类名
再来聊聊:has()。去年Safari和Chrome都支持了之后,这个选择器已经能用在大项目里了,不是用来玩的。
最常见的场景:卡片里有一个图片,我们希望当图片加载失败或者没有图片时,卡片布局完全变个样。以前的做法是后端给一个空字段,前端加个类名如“no-image”,然后写样式。
现在你可以直接把规则绑在“结构关系”上:
.card:has(> .cover) {
padding: 0;
}
.card:not(:has(> .cover)) {
padding: 20px;
}
这里>表示直接子元素,意思是“如果卡片有封面子元素,就走这套样式;没有封面,就另一套”。你根本不用去后台判断有没有图,也不用在渲染时动态拼接类名,样式自己根据内容变化了。
另一个更常用的:表单校验提示。以前你得在一个div上同时挂class=”error”和hidden,然后前端去切换。现在可以这样写:
.form-group:has(input:invalid) .error-text {
display: block;
}
.error-text {
display: none;
}
只要输入框状态是invalid,错误提示自动显示出来。少掉几十行JS有没有?
四、层叠层:防覆盖的唯一正确方式
接下来是CSS里一个非常底层但很多时候被人忽略的api——@layer层叠层。说穿了就是定义谁先谁后,让样式表的管理不再靠“文件引入顺序”和“加!important”来维持。
以前一个项目里有antd的样式、有公共老代码、有页面新写的样式,后加载的总是覆盖先加载的。一旦引用了第三方组件库,调样式经常得靠:在后样式里加长选择器去压第三方样式。
用层叠层以后,你可以明确给样式分组:
@layer base, components, utilities;
上面定义了三个层的顺序,从前往后优先级递增。utilities层最后,所以utility的样式一定会盖过components层。再也不用纠结谁前谁后。
然后把你的样式分放到不同层里:
@layer components {
.button {
background-color: #3b82f6;
color: white;
}
}
@layer utilities {
.bg-red {
background-color: red;
}
}
现在就算你在components层里写.button设置背景色,只要按钮元素同时有.utility类的bg-red,最终背景就是红色的,因为越靠后越优先,跟引入顺序无关了。
当然你可以把第三方库放到components外面,或者用我这样的方式,官方组件库包一个层,自己的业务样式放一个层,修改功能的时候根本没有选择器的烦恼。
五、实战案例:重构一个通用粉丝卡片
把上面三个技术用到一个有点复杂度的组件上。需求:做一个用户关注卡片,显示头像、昵称、简短介绍、粉丝数、关注按钮。这个卡片可能出现在用户主页(宽侧边栏),也可能出现在推荐列表(窄卡片栏),甚至出现在弹窗底部的推荐关注(大概是300px左右宽度)。
如果用老办法,你得写两套样式加媒体查询,还得给不同位置的卡片起不同类名。现在我们用新特性一次搞定。
HTML结构
<div class="follow-card-wrap">
<article class="follow-card">
<img class="avatar" src="user2.jpg" alt="avatar" />
<div class="follow-info">
<h3>@前端大刘</h3>
<p>曾就职于字节,写代码也写前端故事</p>
</div>
<button class="follow-btn">+ 关注</button>
</article>
</div>
样式实现
/* 第一层:定义层顺序 */
@layer layout, component, utility;
/* 把容器查询的规则放进component层 */
@layer component {
.follow-card-wrap {
container-type: inline-size;
container-name: followcard;
}
.follow-card {
display: flex;
flex-direction: column;
background: #f9fafb;
border-radius: 16px;
padding: 20px;
gap: 12px;
transition: all .2s;
}
/* 头像默认是大图,圆角比较深 */
.avatar {
width: 60px;
height: 60px;
border-radius: 30%;
object-fit: cover;
}
.follow-info {
flex: 1;
}
.follow-info h3 {
font-size: 18px;
margin: 0 0 4px;
}
.follow-info p {
font-size: 14px;
color: #6b7280;
line-height: 1.4;
margin: 0;
}
/* 按钮默认是主题色 */
.follow-btn {
border: 0;
background: #2563eb;
color: #fff;
padding: 10px 16px;
border-radius: 999px;
font-weight: 600;
cursor: pointer;
}
/*
* 容器宽度 ≥ 400px 时
* 卡片变成横向布局,头像放大,按钮独立在右侧
*/
@container followcard (min-width: 400px) {
.follow-card {
flex-direction: row;
align-items: center;
padding: 18px 24px;
}
.avatar {
width: 72px;
height: 72px;
border-radius: 50%;
}
.follow-btn {
margin-left: auto;
align-self: center;
}
}
/*
* 容器宽度 ≥ 600px 时
* 卡片和左侧内容之间有更多留白,信息区可以展示更多
*/
@container followcard (min-width: 600px) {
.follow-card {
padding: 28px 32px;
}
.follow-info p {
font-size: 16px;
max-width: 60ch;
}
}
/*
* 如果卡片没有头像,
* 使用:has()移除内边距并改为紧凑模式
*/
.follow-card:not(:has(.avatar)) {
padding: 12px 16px;
}
.follow-card:not(:has(.avatar)) .follow-info {
padding-left: 6px;
}
}
写完之后你会发现,页面中不管什么地方放这个卡片组件,它都会自己判断宽度调整样式,完全不用写第二套。你只需要在容器放入这个组件,然后给它自由缩放。这感觉跟写React组件一样,只需要让组件自己管理自己的CSS。
六、踩坑与注意事项
用这几个新特性,目前有几个点要在项目中注意:
- container-type会影响子元素能否设置百分比高度。如果容器内部有需要按高度百分比的元素,建议改成container-type: size,但那样性能要差一点,看需求。
- :has()在性能上还是稍微有点开销,尤其是选择器特别长的时候。尽量避免在产品列表里每一条都写复杂的:has(),实测在几千条DOM上会有卡顿。可以用在局部区域,别用在全局通用节点上。
- @layer浏览器兼容性:Chrome/Edge和Firefox都支持,但Safari在15.4以后才支持。查了一下,现在主流浏览器都ok,但如果你还要兼容老版本Safari,建议把@layer用旧语法写一遍fallback。
七、为什么说这是组件化CSS的正确方向
之前的CSS不是不能组件化,是缺了这些基础能力。媒体查询管视口,你没法处理组件在父容器里的变化;类名切换可以控制状态,但多状态的时候容易记混;靠引入顺序控制覆盖,迟早会被某一次全局升级给坑。
容器查询、:has()、@layer这三个组合起来,组件自己感知自己的尺寸,自己根据内容调整自己的状态,而且样式优先级完全掌握在你手里。这套玩法已经可以在生产环境用了,尤其是内部系统或者采用现代浏览器的项目,为什么不马上用起来?
如果你还没去试,我强烈建议开个简单页面把上面那段代码复制过去跑一跑。改变不是来自别的框架,就是来自这些看似基础的新增CSS规则。写代码这么多年,还是觉得这种能彻底改变开发方式的小技巧最让人兴奋。

