编程 卡片在主栏和侧栏长得不一样:CSS 容器查询的用法与回退

2026-09-26 00:03:30

卡片在主栏和侧栏长得不一样: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+。

推荐文章

程序员茄子在线接单