
CSS 容器查询:一次不声张的组件重构
卡片在侧栏里挤成一团
公司的后台有个卡片组件,ProductCard。图、标题、两行描述、一个按钮,用媒体查询写的:
@media (max-width: 1024px) {
.product-card { padding: 12px; }
.product-card__desc { display: none; }
}
问题出在侧栏。侧栏 280px,主栏 700px,在同一个断点上行为却一样:窗口拉大,侧栏的卡片也展开成宽屏的样子,描述露出来,挤成四行;窗口缩小,主栏的卡片反倒把描述藏了,明明它还有富余。
断点描述的是屏幕,不是组件。
这个 bug 是运营先发现的。她在侧栏里截了张图问我,为什么同样一张卡片,在这里只剩两行字。我打开页面量了半天,才意识到媒体查询根本不知道侧栏有多宽,它只知道窗口有多宽。
先量,再改
动手之前我做了一件很土的事:在浏览器里把后台每个容器量了一遍。主栏内容区 704px,右侧栏 288px,中间还有一个 380px 宽的抽屉。三个宽度,同一个卡片,而媒体查询按 1024 和 768 两个断点切的,正好把它们切得乱七八糟。
我把这些数字写在一张便签上贴在显示器边:卡片要在 280、380、700 这三种宽度下都说得过去。这三个数后来变成了容器查询的阈值。
改之前我先写了一个只有三张卡片的 demo 页,把侧栏、主栏、抽屉三种宽度并排摆着,改一段就缩一次窗口,省得在真实页面里来回点。
让组件问自己的父级
容器查询把参照物从视口换成最近的有尺寸的祖先。
.card-list {
container-type: inline-size;
container-name: products;
}
@container products (min-width: 420px) {
.product-card {
display: grid;
grid-template-columns: 112px 1fr;
gap: 16px;
}
}
@container products (max-width: 419px) {
.product-card__desc {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
inline-size 只关心宽度,不会因为高度触发查询,也不用给容器固定高度。我一开始写成 size,列表在块方向上的尺寸一下塌成 0,才明白两者的区别。
两个坑
第一个是命名。查询只认最近一个声明了 container-type 的祖先,而我们外层那个 .panel 恰好也写了 container-type: inline-size。@container products 匹配不到,样式静默失效,控制台一句话都不给。把名字认真写上,就是为了让这种失效有迹可循。
第二个是 containment 的副作用。容器自己的布局不再受内容影响,项目里一个靠 height: 100% 撑满的内层忽然归零;卡片里那个 position: fixed 的下拉菜单,也被容器约束着跑进了裁剪区。前者改成 min-height,后者换成 absolute 加一个 relative 的定位层。
改完不算优雅,但每一处都能说出理由。顺手把标题的字号也交给容器:font-size: clamp(15px, 2.6cqw, 18px),侧栏里是十五像素,主栏里是十八,中间连续过渡,不用再写断点。
兜底与代价
- 删掉四个媒体查询断点,六十多行 CSS;
- 卡片在什么容器里都能自己决定长相:侧栏里竖排紧凑,主栏里横排舒展;
- 代价是浏览器基线。Safari 16 之后才稳,我们最低支持 Safari 17,刚好够用;再老一点的浏览器,就得留着媒体查询当兜底。
老浏览器留了一段兜底,只在页面级布局里保留一个断点:
@supports not (container-type: inline-size) {
@media (max-width: 1024px) {
.product-card__desc { display: none; }
}
}
这套写法后来被别的项目抄了过去。有人把 container-type 直接写在卡片自己身上,然后疑惑为什么 @container 一直不生效——查询的是父级,不是它自己。这个坑我该早点写进文档。
上线那天没有人注意到差别。这大概是组件重构能拿到的最好的评价。