响应式的真正输入:为组件重新定义可用空间
一个商品卡放在主内容区时是横向图文结构;放进侧栏后,文字挤压、操作按钮换行;放进弹窗的双栏区域后,它又可能因为页面视口足够宽而继续套用"大屏样式"。
很多团队遇到这类问题时,会继续补充 @media 断点:给侧栏加覆盖规则、给弹窗加特殊选择器,再为紧凑模式增加一个页面宽度条件。组件看似适配了更多场景,实际上却越来越依赖它所在的页面、DOM 分支和祖先类名。
问题不在于断点数量不够,而在于组件读取了错误的输入。
视口媒体查询读取的是全局环境;但一个可复用组件真正关心的,往往是自己此刻获得了多少可用空间。CSS Container Queries 的意义正在于此:它让组件能够基于祖先容器的尺寸决定内部布局,而不是把浏览器窗口宽度误当作组件宽度。
本文不把它当作 @media 的替代语法,而把它视为一次布局依赖边界的重构。

先分清四类职责:不要让 Container Queries 接管一切
响应式布局中的能力并不互相替代,它们处理的是不同问题。
| 能力 | 应回答的问题 | 典型场景 |
|---|---|---|
@media |
用户所处的全局环境是什么? | 页面导航折叠、深浅色偏好、触控能力、减少动画、全页双栏改单栏 |
| Grid / Flexbox | 当前空间怎样自然分配? | 卡片自动换列、按钮同行或换行、内容按剩余空间伸缩 |
| Container Queries | 组件获得的局部空间是否跨过内容临界点? | 卡片从纵向变横向、工具栏显示完整文案、数据面板切换摘要布局 |
| JavaScript 状态 | 组件处于什么交互或业务状态? | 菜单是否打开、筛选是否展开、权限是否允许操作 |
不是所有媒体查询都应该迁移。
以下仍然属于视口或环境条件:
- 页面级导航在小屏上收起;
- 整个应用从两栏编排变为单栏;
prefers-reduced-motion、prefers-color-scheme、hover、pointer等用户偏好和设备能力;- 与浏览器窗口或打印介质直接相关的全局排版策略。
当规则的真实含义是"这个组件被放进更窄的父区域后,应换一种信息组织方式",它才是容器查询的候选对象。
Container Query 改变的不是样式,而是依赖方向
传统写法常常隐含着这样的关系:
css
.product-card {
display: grid;
grid-template-columns: 9rem 1fr;
}
@media (max-width: 48rem) {
.product-card {
grid-template-columns: 1fr;
}
}
这段代码表达的是:只要视口窄,卡片就堆叠。
但在 1440px 宽的桌面视口里,侧栏中的卡片也可能只有 280px 宽;反过来,移动端横屏中的全宽卡片也可能有足够空间横向排列。
将依赖改为容器后,表达才与组件的真实需求一致:
css
.product-card-shell {
container-type: inline-size;
}
.product-card {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@container (inline-size >= 32rem) {
.product-card {
grid-template-columns: 9rem minmax(0, 1fr);
align-items: start;
}
}
这里有两个角色:
.product-card-shell是查询容器,向后代提供"当前可用内联尺寸"这一布局输入。.product-card是响应者,在容器达到32rem时切换结构。
同一个卡片无论被放在主栏、侧栏、抽屉还是弹窗中,都会根据自己的可用空间工作。组件不再需要知道页面断点,也不需要通过 .sidebar .product-card 这类选择器猜测上下文。
查询容器为什么需要 container-type
尺寸容器并不是任意元素自动拥有的能力。要让元素成为尺寸查询容器,需要设置:
css
.component-region {
container-type: inline-size;
}
也可以使用简写,同时指定名称和类型:
css
.component-region {
container: component-region / inline-size;
}
inline-size 查询的是书写方向上的内联轴。在常见的横向书写模式中,它通常可以近似理解为宽度。卡片、工具栏、表单行和数据面板通常只需要根据横向可用空间改变结构,因此没有必要查询高度。
size 则允许同时查询内联轴和块轴尺寸:
css
.preview-pane {
container-type: size;
}
size 会建立两个轴向的尺寸包含关系。这样可以避免后代因查询结果改变样式、样式又反过来改变容器尺寸的循环,但代价是容器不能在相应轴上继续依赖后代尺寸自然撑开。若父级没有提供明确的尺寸上下文,尤其是在块轴上,可能出现尺寸计算或塌缩问题。
因此,工程上的默认策略应当是:
- 优先使用
inline-size:绝大多数横向布局模式切换都足够; - 仅在确实需要查询高度、纵横比或块轴条件时使用
size; - 建立容器前,先确认它的尺寸来自父级网格轨道、Flex 分配、块级拉伸或显式尺寸,而不是寄希望于后代撑开它。
容器应当是一个能够独立提供可用空间的布局节点。
谁应该成为容器:按"提供空间的人"划分边界
一个实用判断是:
谁决定组件实际可用宽度,谁就更适合成为查询容器。
例如:
html
<section class="dashboard-grid">
<aside class="summary-slot">
<article class="metrics-panel">...</article>
</aside>
</section>
如果 .summary-slot 是 Grid 的一个轨道,真正决定 .metrics-panel 可用宽度的是它,那么应将容器建立在 .summary-slot,而不是整个 .dashboard-grid,更不是 body。
css
.summary-slot {
container: metrics-slot / inline-size;
}
这样定义后,数据面板的契约就变成"依赖插槽提供的空间",而不是依赖仪表盘究竟是两栏、三栏,还是嵌在弹窗中。
反过来,以下两种做法通常会制造新的耦合。
把过大的祖先设为容器
若把页面根节点或整个主内容区设为容器,侧栏里的组件仍然会读到一个过大的尺寸,问题并没有解决。容器虽然距离组件较近,但语义边界仍然太远。
每一层 DOM 都设为容器
容器过多会让"最近合格祖先"难以预测。后续有人在组件外多包一层布局节点,就可能使内部查询改为读取新的尺寸来源。容器不是通用标记,应只放在真正提供局部布局上下文的节点上。
从旧 @media 迁移前,先给规则做依赖诊断
存量项目最危险的做法,是把所有 @media 批量替换成 @container。正确的起点不是改语法,而是逐条追问:这条规则究竟依赖什么?
1. 组件自身断点:适合迁移
特征是:组件换到不同父容器时就可能失效。
- 卡片从图文横排切为上下堆叠;
- 过滤工具栏从"输入框加多按钮"切为纵向分组;
- 表格摘要区隐藏次要指标;
- 操作区从按钮文字改为图标优先布局。
这些规则应由组件可用空间触发。
2. 页面编排断点:保留在媒体查询
特征是:变化针对整个页面的信息架构,而非某个组件内部。
- 桌面端显示侧栏,窄屏改为侧栏抽屉;
- 应用主框架从三列变一列;
- 顶部导航从完整导航切为菜单入口。
这些规则描述的是页面如何占用屏幕,因此仍然属于视口响应式。
3. 自然伸缩与装饰变化:先不写查询
有些问题根本不需要断点:
css
.card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
}
auto-fit、minmax()、flex-wrap、min-width: 0 和弹性尺寸,本来就能处理连续的空间分配。只有当组件需要切换布局模式、改变信息优先级或变更结构关系时,才使用容器查询。
贯穿案例:把数据面板变成可组合组件
假设一个数据面板可以放在仪表盘主区、窄侧栏和弹窗中。旧代码通常会把所有情况绑在视口上:
css
.metrics-panel {
display: grid;
grid-template-columns: 8rem 1fr auto;
gap: 1rem;
}
@media (max-width: 64rem) {
.metrics-panel {
grid-template-columns: 1fr auto;
}
.metrics-panel__chart {
grid-column: 1 / -1;
}
}
@media (max-width: 40rem) {
.metrics-panel {
grid-template-columns: 1fr;
}
}
它的隐患是:主区和侧栏共享了同一套视口结论。于是,在宽屏侧栏中,面板仍被当作宽屏组件渲染。
可以按以下方式重构。
第一步:让插槽提供尺寸上下文
css
.metrics-slot {
container: metrics / inline-size;
}
第二步:让基础样式在最窄空间也可用
css
.metrics-panel {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.875rem;
}
.metrics-panel__actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
基础状态不应假设"桌面优先"的复杂三列,而应保证信息完整、内容不溢出。这也是不支持容器查询时的回退基础。
第三步:只在内容临界点切换模式
css
@container metrics (inline-size >= 28rem) {
.metrics-panel {
grid-template-columns: 7rem minmax(0, 1fr);
}
.metrics-panel__chart {
grid-column: 1 / -1;
}
}
@container metrics (inline-size >= 46rem) {
.metrics-panel {
grid-template-columns: 8rem minmax(0, 1fr) auto;
align-items: center;
}
.metrics-panel__chart {
grid-column: auto;
}
.metrics-panel__actions {
justify-content: end;
flex-wrap: nowrap;
}
}
这里的 28rem 与 46rem 不对应手机、平板或桌面设备,而对应面板内容开始拥挤、以及图表、指标和操作区能够同时保持可读性的临界点。
这就是容器断点应有的来源:内容失败的位置,而不是设备型号表。
嵌套组件:默认查询最近容器,命名只用于消除歧义
未命名的 @container 会选择最近的合格祖先容器。这对于"卡片内部的操作栏只关心卡片宽度"非常自然:
css
.card {
container-type: inline-size;
}
.card__toolbar {
display: flex;
gap: 0.5rem;
}
@container (inline-size < 22rem) {
.card__toolbar {
flex-direction: column;
align-items: stretch;
}
}
复杂页面中,后代有时需要读取更高层的布局插槽,而不是最近的卡片容器。这时使用名称:
css
.dashboard-column {
container: dashboard-column / inline-size;
}
.metrics-panel {
container: metrics-panel / inline-size;
}
@container dashboard-column (inline-size >= 52rem) {
.metrics-panel__comparison {
display: block;
}
}
命名容器的目的不是让 CSS 看起来更正式,而是明确依赖来源。应遵守两个约束:
- 名称描述布局角色,如
dashboard-column、dialog-content,而不是视觉结果,如wide、desktop; - 只有当最近容器不是正确输入,或存在多个候选容器时才命名,避免为每个组件制造全局命名表。
如果一个后代确实要同时满足两个层次的条件,可以嵌套 @container。嵌套查询应分别针对不同的祖先容器,并且所有条件都必须成立;不要试图把多层布局上下文压缩成一个模糊的大容器。
容器单位:用于组件内部尺度,不用于抢占布局分配
容器查询单位包括:
cqi:查询容器内联尺寸的 1%;cqb:查询容器块尺寸的 1%;cqw、cqh:查询容器宽度、高度的 1%;cqmin、cqmax:两轴中较小或较大的相对值。
它们适合表达"随组件空间平滑变化"的内部尺度,例如标题大小或内边距:
css
.metrics-panel__title {
font-size: clamp(1rem, 0.9rem + 1cqi, 1.5rem);
}
.metrics-panel__body {
padding-inline: clamp(1rem, 3cqi, 2rem);
}
但需要克制:
- 元素占父容器多大比例,优先使用
%; - 多个兄弟如何分配剩余空间,优先使用 Grid 的
fr或 Flex; - 全局排版基准,优先使用
rem; - 仅因空间不足发生的结构变化,优先使用
@container断点。
如果元素找不到符合条件的查询容器,容器单位会使用其规范定义的回退尺寸,通常表现为基于小视口的尺寸。对关键尺寸,最好保留不依赖容器单位的基础值,并在确认容器存在的场景中再增强。
一条可执行的迁移路径
对于大型代码库,建议按风险从低到高迁移,而不是重写所有响应式样式:
- 保留现有媒体查询作为行为基线。 不要一开始删除旧规则,先确保已有页面继续工作。
- 挑选复用频率高、跨插槽使用的组件。 卡片、筛选工具栏、摘要面板通常比页面导航更适合作为第一批对象。
- 列出每条规则的真实依赖。 标记为组件空间、页面编排、环境偏好或自然流式布局。
- 为组件外部插槽建立最小容器。 优先使用
inline-size,并确保插槽本身有稳定的尺寸来源。 - 从基础流式布局开始。 在不支持容器查询或容器极窄时,内容仍应可读、可操作、不溢出。
- 只迁移模式切换规则。 用内容临界点替换设备宽度;不要把连续微调拆成许多容器断点。
- 清理页面特例。 当组件不再依赖
.sidebar、.modal、.desktop-layout等祖先选择器时,再删除这些覆盖规则。 - 在隔离环境验证。 至少测试组件位于窄栏、常规栏、宽栏,以及嵌套在另一个容器中的场景。

容易踩中的六个坑
1. 容器尺寸不符合预期
最常见原因不是 @container 写错,而是容器放错了位置。先在开发者工具中确认:组件实际查询的是哪个祖先、该祖先的尺寸是多少、尺寸由什么布局规则提供。
2. 用 size 后高度塌缩
如果只是横向重排,请改用 inline-size。若必须查询高度,则需要为容器提供独立、可计算的块轴尺寸,而不能依赖子内容反向决定。
3. 容器查询写在容器自身上
容器查询影响的是容器的后代,不应依赖查询结果直接改变被查询容器自身。把"容器外壳"和"需要变化的内部组件"分开,既符合模型,也更容易维护。
4. 用容器查询代替 Grid/Flex 的自然能力
"每少 100px 就减一列"通常是 Grid 的工作,不是 @container 的工作。只有列数变化伴随内容结构、信息层级或操作模式变化时,查询才有价值。
5. 断点变得更碎
容器查询降低了组件对页面的耦合,但不会自动带来好设计。每个断点都应对应一个可描述的内容临界点,例如"操作按钮不能保持同一行"或"摘要与图表不能同时可读"。
6. 误把新能力混进主线
Style Queries 和 Scroll-State Container Queries 属于不同类型的容器能力:前者围绕容器的计算样式,后者围绕滚动、吸附或 sticky 等状态。它们不应被当作尺寸容器查询的同义词。除非需求确实是主题令牌驱动样式或滚动状态驱动展示,否则应先把尺寸依赖边界做好。
渐进增强:把"无容器查询"当作正常基线
尺寸容器查询已经获得主流现代浏览器支持,但项目是否需要兼容旧版企业浏览器、旧内嵌 WebView 或其他特殊运行环境,仍应由真实用户基线决定。
兼容策略不必复杂。关键原则是:基础布局先可用,容器查询只是增强。
css
.metrics-panel {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@supports (container-type: inline-size) {
.metrics-slot {
container-type: inline-size;
}
}
@container (inline-size >= 46rem) {
.metrics-panel {
grid-template-columns: 8rem minmax(0, 1fr) auto;
}
}
这里将 @supports 仅用于保护容器声明;不支持 @container 的浏览器会忽略对应的容器查询规则,组件仍停留在基础单列或自然换行模式。@supports 不能替代真实设备、目标 WebView 和视觉回归测试。
把组件断点当作组合契约管理
当 Container Queries 被用于设计系统,断点不再是页面样式里的私有数字,而是组件对宿主环境提出的组合契约。
例如,可以把数据面板的状态命名为:
compact:指标、图表与操作区无法并列;standard:摘要与图表可分区展示;expanded:操作区、比较数据与辅助说明可同时出现。
这些名称描述内容能力,而不是 mobile、tablet、desktop。组件文档还应记录:每个状态需要什么最小空间、会隐藏或重排哪些信息,以及在哪些容器宽度矩阵中完成验证。
最终,Container Queries 带来的不是"多一组 CSS API",而是一条更健康的依赖原则:
页面负责给出空间,组件负责解释空间。
当组件不再从全局视口猜测自己该长什么样,它才能真正被放入不同的页面、插槽和产品流程中,而不需要为每一次组合新增一条例外规则。