容器查询(CSS Container Queries)允许组件基于父容器的尺寸来改变样式,而不是像媒体查询那样盯着整个浏览器视口。这玩意儿就像给每个组件装了个独立的“感应器”,组件被放到任何地方都能自适应,不像以前那样一旦挪个窝就翻车。目前Chrome和Safari已经支持得挺稳,Firefox 109版也开始跟进了。
搞前端布局时,总遇到卡片在窄视口里字体挤爆、图标线条时粗时细的破事。以前靠媒体查询硬扛,结果换个位置就全乱套。后来试了容器查询,发现三个真实场景能直接救命:卡片网格内边距自动缩放、布局方向智能翻转、SVG描边粗细按需变化。每个场景都给出从翻车到真香的实操代码,小白照着敲就能跑通。特别注意容器查询单位(cqw)和em单位的混用陷阱,以及组件化设计中的props冲突问题。
卡片网格
先聊个烂大街但贼烦人的需求:做一组卡片网格,要求每张卡片保持1:1比例,并且卡片标题字号随着卡片宽度缩小而自动减小。用媒体查询搞的话,只能监听视口宽度,但卡片可能在侧边栏、弹窗、主内容区等不同地方出现,视口宽度根本管不住它们各自的实际尺寸。
以前硬写的方案得这样:监听窗口resize事件,算每张卡片的宽度,然后给每张卡片内联设置一个基于宽度的字号,里面所有元素用em单位。代码量感人,性能还吃紧。
// 翻车写法示例(千万别照搬,性能拉胯)
window.addEventListener('resize', () => {
document.querySelectorAll('.card').forEach(card => {
let w = card.offsetWidth;
card.style.fontSize = (w / 20) + 'px';
});
});如果当时能用容器查询,直接给.card声明成一个容器:
.card {
container: card / size;
}
.card__inner {
padding: 10cqw; /* 内边距总是卡片宽度的10% */
}这里的10cqw就是容器宽度的10%。假设卡片宽500px,内边距就是50px。而且不管卡片被扔到侧边栏还是弹窗里,这个比例永远成立。一个更骚的玩法:把字号设成5cqw,内边距用2em。因为em相对于当前元素的字号,而字号又是5cqw,所以内边距实际等于10cqw,一箭双雕。写这段代码时注意:cqw单位找的是最近的容器祖先,如果直接把font-size: 5cqw写在.card上,它反而会去找上级容器,所以必须包一层.card__inner来承接。
布局切换妙招
另一个项目里需要做信息卡片,宽屏时图片在左文字在右(横排),窄到一定程度就变成图片在上文字在下(竖排)。但最坑爹的是,这个切换点在不同页面里完全不一样——因为父容器宽度被各种侧边栏挤压。用媒体查询写死max-width: 600px的话,到了侧边栏里600px可能已经超出父容器了,布局直接崩。
如果当时能用@container规则,代码清爽到爆:
.info-card {
container-type: inline-size;
container-name: info-card;
}
@container info-card (max-width: 500px) {
.info-card__inner {
flex-direction: column;
}
}注意container-type: inline-size表示只监听内联方向(即水平宽度)变化,省性能。而container-name给容器起个名,防止多个容器互相干扰。但有一个大坑:如果一个组件内部还嵌了其他依赖JS props的子组件(比如按钮的颜色主题靠data-variant控制),那容器查询没法改JS属性。这时候要么把整个设计系统全部基于容器查询重写,要么就别硬塞,否则样式会重复维护两套。比如子组件在竖排模式下需要加粗边框,但JS里没对应状态,CSS改不了,最后只能用额外类名来补,越补越乱。
图标描边控制
第三个场景是标题旁边跟着一个SVG图标,要求图标尺寸跟随标题字号自动缩放,同时描边粗细不能线性变化——太细时看不清,太粗时糊成一坨。用em单位可以让图标宽高等于1em,描边用stroke-width: 0.8,但这玩意儿在所有尺寸下都是0.8,小图标上0.8太细,大图标上0.8又太粗。
以前只能手动给不同尺寸的标题配不同类名,比如.heading-large .icon设stroke-width: 1.5,.heading-small .icon设stroke-width: 3。但标题字号如果是流体变化(比如clamp(1rem, 5vw, 3rem)),类名根本跟不上。
容器查询的解法:把.icon设为一个容器,然后根据它的宽度分档位切换描边粗细。
.icon {
container: icon / size;
width: 1em;
height: 1em;
}
.icon svg {
width: 100%;
height: 100%;
fill: none;
stroke: #ccc;
stroke-width: 0.8;
}
@container icon (max-width: 70px) {
.icon svg { stroke-width: 1.5; }
}
@container icon (max-width: 35px) {
.icon svg { stroke-width: 3; }
}| 容器宽度 | 描边粗细 | 适用场景 |
|---|---|---|
| >70px | 0.8 | 大标题旁 |
| 36-70px | 1.5 | 中号标题 |
| ≤35px | 3 | 小图标 |
注意这里.icon的宽高写死了1em,这个em是相对于它的父元素(比如<h2>)的字号,所以图标尺寸会自动跟随标题。而容器查询监听的宽度是.icon元素的实际像素宽度,当标题字号变小时,.icon的像素宽度也变小,触达70px或35px门槛时描边自动跳档。测试时记得把浏览器缩放拉一拉,看描边变化是不是丝滑切换。
额外玩法
除了宽度和高度,容器查询还能监听orientation(横竖屏)和aspect-ratio(宽高比)。比如:
@container info-card (orientation: landscape) {
/* 横排时搞点骚操作 */
}
@container info-card (aspect-ratio: 3/2) {
/* 当容器宽高比正好3:2时触发 */
}这俩目前还没在实际项目里用上,但想象一下:一个视频卡片组件,当它的宽高比变成16:9时自动播放预览,或者当容器变成竖屏时把控制按钮移到右边。不过注意aspect-ratio触发条件比较苛刻,得精确匹配,一般配合min-aspect-ratio或max-aspect-ratio用更实际。
