卡片在主栏和侧栏长得不一样:CSS 容器查询的用法与回退
媒体查询看不到容器
一个 .card 组件,放进主栏宽度 720px,塞进侧栏只剩 320px。用 @media (min-width: 700px) 的时候,只要视口够宽,侧栏里的卡片也会被当成宽卡片渲染——媒体查询只看视口/设备全局属性,不关心组件被塞进了哪个父级。
换参照系。容器查询(@container)按最近祖先容器(container)的尺寸/样式给后代应用样式,与视口无关。同一份组件 CSS 放到任意位置,表现跟着容器走。
- MDN:
- web.dev 容器查询:
- Josh Comeau 教程:
- Can I use:
为什么拖了二十年:循环
容器查询里的 CSS 可能改变容器自身的尺寸,条件随之失效,重新计算又变回来——无限循环,UI 抖动。CSS 工作组一度认为这件事二十年都做不了。
解法是把 DOM 切片做成自包含:Containment API 里的 contain: size / contain: inline-size,让声明的父元素不再动态响应内容尺寸,也就不会被后代反过来撑大。父级尺寸不随内容变,就能当一把稳定的测量标尺,循环被短路。container-type: size/inline-size 做的就是这件事。
由此得到一条铁律:不能改变你测量的东西。@container 规则里的选择器只能选中容器内的后代元素,不能给容器本身设样式。
container-type:size / inline-size / normal
用法两步:先用 container-type 声明局限上下文(containment context),再用 @container at 规则查询。
size:行向和块向都做尺寸限制,可查width/height/aspect-ratio/orientation。副作用明显——元素尺寸不再依赖内容,不给显式尺寸就塌陷为 0(只剩 padding)。inline-size:只限行向,阻止后代影响其内联尺寸,可查width,不能查height/aspect-ratio。宽度由布局显式指定,高度仍随内容。组件里多数场景用它。normal:默认值,不是尺寸查询容器,但仍是样式查询容器。
.card {
container-type: inline-size;
/* 需要查 height / aspect-ratio 时改用 size,并显式给尺寸 */
/* container-type: size; */
/* width: 100%; height: 400px; */
}
@container 语法、命名与取反
.card {
container-name: card;
container-type: inline-size;
/* 简写:container: card / inline-size; */
}
@container card (min-width: 450px) {
.card h2 { font-size: 2em; }
}
条件支持 width / height / inline-size / block-size / aspect-ratio / orientation,可用比较运算符 >、=,也可以用冒号单值写法。多个条件用 and / or 组合,not 取反:
@container layout not (width >= 450px) {
/* ... */
}
不写名字时,@container 匹配最近的、具有局限上下文的祖先。
cq 长度单位
cqw = 容器宽度 1%,cqh = 高度 1%,cqi = 行向尺寸 1%,cqb = 块向尺寸 1%,cqmin = min(cqi, cqb),cqmax = max(cqi, cqb)。配合 max() / clamp() 做连续缩放:
.card h2 { font-size: max(1.5em, 1.3em + 2cqi); }
.card .actions button {
padding: 0.5em 1.25em;
padding-inline: max(1em, 3cqi);
}
inline-size 容器不能用 cqh / cqb,块尺寸不确定。
样式查询
尺寸之外还能查自定义属性值:
.inventory {
--detail: low-stock;
container-type: inline-size;
}
@container style(--detail: low-stock) {
.badge { background: #ffe2e2; color: #a00; }
}
两点约束:必须用 style() 包裹,才能和尺寸查询区分开;必须给祖先容器设置该自定义属性。目前只支持自定义属性值,不支持查询任意 CSS 属性,也不支持布尔存在性查询(@container style(--detail))。范围查询如 `30% 。
可复用组件的多变体(New / Low Stock 徽章)就是典型触发场景。
浏览器支持与回退
Chrome/Edge 106+、Safari 16.0+、Firefox 110+ 支持,全局覆盖约 94%(Chrome 105 只是 partial)。
不建议全量 polyfill(),API 体积大、性能差。更便宜的做法是用 ResizeObserver 监听容器尺寸,在容器上挂断点类名,再用 @supports 把回退规则框住,让支持容器查询的浏览器忽略它们:
const ro = new ResizeObserver(([entry]) => {
const el = entry.target
el.classList.toggle('is-wide', entry.contentBoxSize[0].inlineSize >= 450)
})
ro.observe(document.querySelector('.card'))
@supports not (container-type: inline-size) {
.card.is-wide h2 { font-size: 2em; }
}
@container (min-width: 450px) {
.card h2 { font-size: 2em; }
}
也可以用 @supports not (container-type: inline-size) 给旧浏览器做网格/弹性布局降级。web.dev 有完整的回退写法:。用 Tailwind 的话可以直接接官方插件:。
Vue 单文件组件
{{ title }}
Low Stock
{{ desc }}
加入购物车
defineProps({
title: String,
desc: String,
stock: { type: Number, default: 99 }
})
defineEmits(['add'])
.card {
container: card / inline-size;
--detail: v-bind('stock = 450px) {
.card__head {
grid-template-columns: 1fr auto;
align-items: center;
}
.card__desc { font-size: 1rem; }
.card__actions { display: flex; gap: 0.5rem; }
}
@container style(--detail: low-stock) {
.badge { color: #a00; }
}
v-bind() 注入的 --detail 落在容器元素自身上,后代 .badge 通过样式查询读到它。这个写法需要 Vue 3.2+。