☰
从媒体查询到容器查询:组件级响应式布局改造实战
2026/10/26 21:57:52 网站建设 项目流程

媒体查询按视口断点排布页面的那套写法,在组件化时代越来越别扭。同样一张卡片,放在主内容区是宽形态,塞进侧栏就要变成窄形态;同一段说明文字,在900px容器里用18px刚好,被塞进320px的移动端小卡片里就溢出三行。更要命的是,这些组件在不同位置的呈现需求完全相反,你却只能用一套全局断点去描述它,结果就是加了一堆作用域不明的类名和覆盖规则。2023年三大渲染引擎全部稳定支持容器查询与容器单位之后,这件事终于有了从根上解决的方案:让组件自己测量自己所在的容器,再根据容器尺寸决定样式;甚至直接用 cqi、cqw 这类容器单位把字号绑到容器宽度上,配合 clamp() 做平滑缩放。这篇内容会从语法骨架、单位计算、完整改造流程一路讲到踩坑记录,适合已经写熟 Flex 与 Grid、但对容器查询还停留在"听说过"阶段的同学,也适合正在维护一套跨多场景复用组件库的同行。

1. 媒体查询的边界在哪里,容器查询补的是哪块短板

1.1 一个卡片组件被复用三次之后的崩溃现场

我手上有个很典型的场景:一套内容卡片组件,同时出现在三个位置——首页主区、右侧推荐栏、以及一个横向滚动的区块。首页主区宽度大概860px,卡片呈现为左图右文的横向布局,标题20px;右侧推荐栏只有280px宽,卡片必须变成上图下文,标题缩到15px;横向滚动区块里每张卡片固定300px左右,但屏幕本身很宽。

用媒体查询写的时候,问题立刻暴露:三处卡片在同一屏内同时存在,视口宽度是一个确定值,你没法让同一个卡片的三种形态同时生效。最后只能写成.card--wide、.card--narrow、.card--scroll三个变体类,在三个父容器上分别打上不同的修饰类。这还算能跑,但组件的形态判断逻辑从"我自己有多宽"错位成了"页面有多宽",只要布局调整、侧栏折叠、拖拽分栏宽度变化,修饰类就得重新配对。更糟的是这个组件还要被别的项目引用,那边根本没有--scroll这个变体,于是又出现了第四种写法。

注意:媒体查询描述的是视口,容器查询描述的是父容器。这不是新旧替代关系,而是测量对象不同。视口级的需求(比如导航栏在窄屏收成抽屉)继续用媒体查询,组件级的需求才交给容器查询。

1.2 容器查询真正解决的三个具体问题

第一个问题是数值匹配问题。Text 组件到底该多大,取决于它所在的槽位有多宽,而不是设备有多宽。容器单位把这个关系直接写进 CSS:font-size: 2.5cqi的意思是"字号等于容器内联尺寸的2.5%",这个声明在哪个容器里就按哪个容器算,不需要任何 JS 和修饰类。

第二个问题是状态爆炸。N个组件形态 × M个放置位置,用媒体查询会膨胀成 N×M 个组合;用容器查询,组件内部维护 N 套形态,位置信息由父容器自动提供,组合数变成 N+M。

第三个问题是可移植性。带容器查询的组件被搬进任何项目,只要父级声明了container-type,它就自动适配,不需要外部补齐断点变量。这一点在跨团队共享组件时价值极高,也是我把原有组件库整体改造的主要动因。

1.3 2023年为什么成了一个节点

容器查询的语法不是一次性落地的。最早的提案里只有尺寸查询,后来拆成了尺寸查询、样式查询、滚动状态查询几部分;容器单位也是在尺寸查询之后才补齐的。2022年下半年,Chromium 系先用上了尺寸查询,WebKit 紧随其后,而 Gecko 引擎把这块补齐是 2023 年初的事。三大引擎都到位之后,才谈得上"敢上生产"。

这个时间差带来一个很现实的影响:如果项目需要兼容较老的浏览器版本,容器查询只能作为渐进增强来用,基础样式必须先用传统写法兜底。我在 2022 年就吃过一次亏——直接拿容器查询写了主结构,结果在老版本浏览器里整个卡片宽度塌成一条线,因为容器单位在没有可用容器时会退化成一个小视口基准值,40cqi算出来小得离谱。所以现在的固定套路是:先写像素或 rem 的基础样式,再用@supports把容器查询的增强包起来。

判断要不要用,我自己的门槛是看这个组件的尺寸变化是否发生在同一屏内。如果是响应式的整页布局调整,媒体查询足够;如果同一个组件要在同一屏内以不同尺寸出现,容器查询才是对工具。

2. 语法骨架:container-type、container-name 与 @container

2.1 container-type 的三种取值与真实代价

要让一个元素成为"可查询容器",必须在它身上声明container-type。取值有三个,差别很关键:

取值建立什么查询上下文附带效果适用场景
normal不建立尺寸查询上下文无只想用container-name给后代做cq定位时的默认态
inline-size可查询内联尺寸(宽度)应用contain: layout style inline-size绝大多数卡片、栏位、组件外壳
size可查询内联与块两个尺寸应用contain: layout style size宽高都固定的容器,如仪表盘格子、画布外框

inline-size是日常主力,因为它只要求容器在行内方向不依赖内容撑开。而size要求容器的两个方向尺寸都由外部决定,一旦容器高度是由内容撑起来的,加上size之后高度会塌成 0,cqh立刻失去意义。

注意:container-type: inline-size会把行内尺寸的尺寸包含打开,意思是子元素再宽也撑不开容器宽度了。以前靠内容把父级撑开的写法,在加了container-type之后会失效,表现为文字溢出或横向滚动。这个副作用是排查里最容易漏的一条。

另外要明确,container-type不会自动让元素成为新的层叠上下文,但contain: layout会影响包含块的计算方式,绝对定位后代的参照物会变成这个容器。这一点在改造带角标、浮动按钮的卡片时要注意,原本相对外层定位的元素可能突然改相对卡片定位。

2.2 container-name 与简写 container

光有container-type只能建立"匿名容器",后代元素用@container时会匹配最近的、类型满足条件的祖先。真实项目里层级往往很深,最近的那个容器未必是你想匹配的,这时候就需要命名。

/* 完整写法 */ .card-slot { container-type: inline-size; container-name: cardbox; } /* 简写:container: <name> / <type> */ .card-slot { container: cardbox / inline-size; }

简写里名字和类型的顺序是固定的,名字在前、斜杠分隔、类型在后。有个细节容易写错:如果只想命名不要尺寸上下文,写container: cardbox / normal,此时该元素能被@container cardbox引用,但引用之后没有尺寸可查,尺寸查询类条件不会命中。命名的作用范围只对本元素的后代生效,容器自己不能被自己的容器查询选中——这条是语法层面的硬限制,后面还会专门讲。

名字建议按"槽位职责"来起,比如sidebar、main、tile,而不是按组件起,因为一个容器里可能住着好几个组件,按职责命名更稳。

2.3 @container 规则的匹配范围与就近原则

@container的写法见下,条件语法跟媒体查询高度一致,支持min-width、max-width、width、inline-size、aspect-ratio、方向查询等:

.card-slot { container: cardbox / inline-size; } .card__title { font-size: 15px; } @container cardbox (min-width: 480px) { .card__title { font-size: 20px; } } @container cardbox (min-width: 760px) { .card__title { font-size: 24px; } .card { grid-template-columns: 180px 1fr; } }

匹配规则有三条必须记住。第一条是就近原则:不写名字时,从当前元素往上找最近的一个满足所需类型的容器。第二条是名字匹配:写了名字,就只往上找同名的容器,中间夹着别的匿名容器不影响。第三条是作用域:@container内部的规则只对"该容器后代中匹配选择器的元素"生效,容器自己不会被命中。

因为容器自己不能被自己查询,所以改造时经常需要多包一层:

<div class="card-slot"> <!-- 建立容器 --> <article class="card"> <!-- 被查询的后代 --> <h2 class="card__title">标题</h2> </article> </div>

如果直接把container-type写在.card上,再想用@container改.card自身的布局就永远不生效。这个坑我见过太多次,几乎是容器查询入门的第一道坎。

3. 容器单位:把字号绑到容器宽度上的正确姿势

3.1 六个单位逐个拆解

容器单位是查询容器的尺寸百分比,跟vw、vh是视口的百分比是一个道理,只是分母换成了容器。

单位含义典型用途
cqw容器宽度的 1%水平方向的尺寸,不受书写模式影响
cqh容器高度的 1%需要container-type: size才有意义
cqi容器内联尺寸的 1%正文流里的字号、间距,书写模式自适应
cqb容器块尺寸的 1%竖向承载的元素尺寸
cqmincqi与cqb中小的那个方形元素、图标
cqmaxcqi与cqb中大的那个需要撑满某一方向的装饰元素

日常用得最多的是cqi,因为它在横排文本里等于宽度,在竖排文本里自动等于高度,不需要为多语言书写模式写两套。cqw更像vw的直系替代品,容易理解但不够通用。cqh要格外小心,容器高度不确定时它算出来可能是 0。

注意:如果元素往上找不到任何满足条件的查询容器,容器单位会退回到小视口尺寸作为基准计算。这时候cqi的表现接近vw,页面在大屏上看着还行,在小屏上字会小到看不清。所以用容器单位的地方,父级一定要显式声明container-type。

3.2 从固定值到手写 clamp 公式(带计算过程)

直接用font-size: 4cqi的问题是两头不可控:容器极窄时字小得没法读,容器极宽时字大得离谱。正确做法是clamp(最小值, 流式值, 最大值)三段式。

先定一个场景:一个卡片标题,希望容器宽 360px 时字号约 15px,容器宽 720px 时约 22px,两端封顶。设流式表达式为a + b × cqi,其中cqi每次代表容器宽度的 1%。

容器宽 360px,1cqi = 3.6px;容器宽 720px,1cqi = 7.2px。设两点满足a + b×3.6 = 15、a + b×7.2 = 22,两式相减得b×3.6 = 7,b ≈ 1.944;代回得a ≈ 15 - 7 = 8。

所以中间项是8px + 1.944cqi,取整成8px + 1.95cqi。最终:

.card__title { font-size: clamp(15px, 8px + 1.95cqi, 22px); }

验算一下:容器 280px →1cqi = 2.8px,中间项8 + 5.46 = 13.46px,被下限 15px 截住,符合"小容器不缩到读不清"的预期;容器 1000px →1cqi = 10px,中间项8 + 19.5 = 27.5px,被上限 22px 截住,符合"大容器不失控"的预期。中间那段是真正在平滑变化的区间。

实际写的时候我建议把 rem 混进来,方便用户调字号:

.card__title { font-size: clamp(0.9375rem, 0.5rem + 1.95cqi, 1.375rem); }

0.5rem在默认 16px 下正好是 8px,跟上面的计算对得上;用户把根字号调到 20px 时,0.5rem变成 10px,整体字号基准会跟着抬升,可访问性更好。这也是我后来不再用纯 px 写中间项的原因。

3.3 一份字号阶梯配置表和比例系数选定思路

手算一次还行,一个项目里十几个组件全都手算就不现实。我的做法是先定阶梯,把常见槽位宽度映射到字号,再统一用同一套系数。

层级最小字号最大字号中间项适用元素
标题一级18px28px10px + 2.5cqi卡片主标题、区块标题
标题二级15px22px8px + 1.95cqi卡片副标题、列表标题
正文14px17px11px + 0.85cqi描述文本、摘要
辅助12px14px10px + 0.45cqi标签、时间、元信息
数字展示24px56px14px + 4.5cqi仪表盘数值

系数怎么定?一个经验规律是:字号相对容器宽度的变化率,正文类控制在 0.5 到 1.0 之间,标题类 1.5 到 3 之间,纯数字展示可以到 4 以上。系数越大越"跟手",但也越容易在中间区间出现明显跳变;系数越小越平稳,但容器变化时不明显。正文类之所以要小,是因为阅读时的字号抖动比视觉冲击更让人不适。

上下限的取值也有讲究。下限建议不低于 14px(正文)和 12px(辅助),再小在移动端就是不合格的阅读体验;上限不要超过容器宽度的 6%,超过之后单行字数太少,读起来眼睛一直要回归行首。

4. 完整实操:三个组件的改造记录

4.1 卡片组件:从媒体查询到容器查询的迁移

原始版本按媒体查询写,长这样:

.card { display: grid; grid-template-columns: 1fr; gap: 12px; } .card__title { font-size: 15px; } @media (min-width: 768px) { .card { grid-template-columns: 180px 1fr; gap: 16px; } .card__title { font-size: 20px; } }

改造三步走。第一步,在卡片的外层槽位加容器声明,不要加在卡片自己身上:

.card-slot { container: cardbox / inline-size; }

第二步,把媒体查询换成对应宽度的容器查询。原来768px的视口断点,换成容器断点时要重新定值——因为卡片在主区只占1fr,视口 768px 时卡片实际宽度可能只有 480px。我的做法是打开开发者工具,量出卡片在三种位置的真实宽度,分别取一个中间值作为断点:

@container cardbox (min-width: 420px) { .card { grid-template-columns: 160px 1fr; gap: 14px; } .card__title { font-size: clamp(15px, 8px + 1.95cqi, 20px); } } @container cardbox (min-width: 640px) { .card { grid-template-columns: 200px 1fr; gap: 20px; } .card__title { font-size: clamp(18px, 10px + 2.1cqi, 26px); } }

第三步,把描述文字、标签、图片圆角这些细节一起收进容器查询——这些才是让组件"看起来真的适配了"的关键,只改字号往往还是别扭。图片在窄形态下用 16:9,宽形态下用 4:3,圆角从 8px 到 12px,间距从 10px 到 20px,全部按容器断点走。

改完之后我把三个修饰类全删了,.card--wide、.card--narrow、.card--scroll一共六十多行覆盖规则清零。这是改造里最爽的部分,但前提是断点值取得准。

注意:容器查询的断点是按容器内容盒宽度算的,如果你给槽位加过padding,测量值会小于你肉眼看到的占位宽度。断点定值前一定要在开发者工具的布局面板里看容器实际的content-box宽度,别按截图估。

4.2 侧栏与主区的同一组件双形态

第二个改造对象是文章列表项。主区里是"缩略图 + 标题 + 两行摘要 + 元信息",侧栏里只有"标题 + 一行时间",缩略图和摘要都要隐藏。

这里我踩过一个坑:一开始想用@container直接改列表项内部元素的display,写了@container (max-width: 360px) { .item__thumb { display: none } },结果不生效。原因是同一条规则里我还写了@container (min-width: 361px) { ... }试图做互补,而两个匿名容器条件叠加时,就近原则会找到不同的容器,逻辑就乱了。修法是给两个容器都命名,让每个@container都指名道姓。

.article-main { container: mainflow / inline-size; } .article-aside { container: sidebarflow / inline-size; } @container sidebarflow (max-width: 360px) { .item__thumb, .item__excerpt { display: none; } .item__title { font-size: clamp(14px, 11px + 0.9cqi, 16px); } .item { gap: 8px; padding: 10px 0; } } @container mainflow (min-width: 620px) { .item { grid-template-columns: 140px 1fr; } .item__excerpt { -webkit-line-clamp: 2; } }

命名之后还有一个好处:同一份组件在两处位置用的是完全不同的一套规则,互不干扰,后续改动不用担心改一处崩另一处。这也是我后来强制团队里所有容器必须有名字的原因——匿名容器在简单场景够用,一旦同屏出现两个容器,匿名就成了定时炸弹。

4.3 仪表盘里的数字与图表容器

第三个场景需要container-type: size。仪表盘的格子是固定的行列网格,每个格子宽高都由 Grid 决定,跟内容无关,正好符合size类型的要求。

.tile { container: tilebox / size; display: grid; place-items: center; } .tile__value { font-size: clamp(24px, 14cqi, 56px); font-variant-numeric: tabular-nums; line-height: 1; } .tile__unit { font-size: clamp(11px, 3.2cqi, 16px); opacity: 0.7; } .tile__spark { width: 100%; height: 40cqb; }

这里有个必须验证的点:.tile的高度是不是在 Grid 里被显式确定。如果它所在的行是auto,高度由内容决定,加上container-type: size之后高度会塌陷,40cqb直接变 0,迷你图表消失。我第一次写就遇到这个,页面上一半的图表格子是空的,排查了半小时才想起来是尺寸包含的问题。修法是把 Grid 的行高写死,比如grid-template-rows: repeat(3, 180px),或者用aspect-ratio: 4 / 3给格子一个确定比例。

另一个经验是数字字号不要用cqw而用cqi,虽然横排时两者一样。原因很实际:这套组件后来被复用到一份竖排布局里,cqw全部失效,cqi一行没改就正常了。这类前期的"多花十分钟",后期能省掉一整轮排查。

至于渐进增强的写法,我固定用@supports包裹:

.tile__value { font-size: 32px; } @supports (container-type: size) { .tile { container: tilebox / size; } .tile__value { font-size: clamp(24px, 14cqi, 56px); } }

@supports的判断条件是声明本身被支持,不是单位被支持,所以判container-type比判cqi更准确。老浏览器会忽略整个块,保留 32px 的兜底值,页面不至于崩。

5. 踩坑清单与排查手册

5.1 写了 @container 却不生效的六个原因

排查容器查询不生效,我基本按这个顺序过一遍,八成问题在前三条。

第一,容器自己不能被查询。@container里的选择器如果选中了建立了container-type的那个元素本身,永远不命中。修法是外面包一层槽位。

第二,忘了写container-type。只写container-name或者只写container-name: xxx / normal,尺寸查询类条件不会命中。

第三,条件里用了容器不支持的尺寸方向。容器是inline-size类型,你去查min-height或者aspect-ratio,不会命中。要查高度必须换成size类型。

第四,断点值和实际宽度对不上。容器有padding,测量的是内容盒;容器被contain: inline-size影响,宽度可能和预期不同。这类问题只能靠开发者工具的容器标记去核对。

第五,被更靠后的同名规则覆盖。容器查询本身不改变层叠优先级,@container内的规则和外面的同权重规则按源码顺序决定胜负,写在后面的赢。我经常遇到"改了没反应",其实是外面那条基础样式写在文件末尾。

第六,@container写进了不能嵌套的位置。它只能写在样式表顶层或另一个条件规则里,不能塞进普通选择器内部。这种错误一般编辑器会报,但复制粘贴时容易漏。

提示:新版开发者工具在建立了查询容器的元素上会显示一个小标记(容器徽章),鼠标悬停能看到容器名称和类型,点击可以高亮当前命中的所有容器查询。这个功能比逐行console快太多,强烈建议先去熟悉一下位置。

5.2 尺寸塌陷、层叠与性能的连带影响

尺寸包含带来的塌陷是最常见的连带问题。container-type: inline-size打开了行内方向的尺寸包含,容器宽度不再受内容影响,所以内部有长单词、长 URL、或者自己不换行的 flex 子项时,会出现溢出而不是撑宽父级。修法是给文本容器加overflow-wrap: anywhere,或者给容器显式width。

size类型的两个方向都包含,风险更大。除了上面说的网格行高问题,还有一种隐蔽情况:容器用position: absolute加inset: 0撑满父级时,按理会得到确定高度,但父级如果本身高度是 auto,绝对定位元素的高度依然是 0,cqb也就是 0。这类问题靠肉眼看不出来,得用开发者工具去看计算的盒模型。

性能方面,容器查询的匹配成本比媒体查询高一点,因为它要跟着布局尺寸变化重新求值,而媒体查询只在视口变化时求值。实际项目里我没观察到明显卡顿,但有两个建议:一是不要给整页所有元素都加container-type,只加在实际需要查询的槽位上;二是避免在滚动中高频改变容器尺寸(比如拖拽分栏),拖拽时容器尺寸每帧都变,cqi字号也会每帧重算,如果发现拖拽卡顿,可以给拖拽中的容器加一个临时类,把字号固定住,松手后再恢复。

还有一条容易忽略:contain: layout会让容器成为后代的包含块,原本相对外层定位的浮层、下拉、Tooltip 会改成相对容器定位,可能被overflow: hidden裁掉。改造带浮层的组件时,要么把浮层提到容器外面,要么给它换定位参照。

5.3 问题速查表与渐进增强写法

现象最可能原因处理方式
@container完全不生效容器自己不能被查询外层加一层槽位并声明container-type
只有部分规则生效条件命中了不同的匿名容器给容器命名,@container指名道姓
查高度不命中容器是inline-size类型改用size,并确保高度确定
容器宽度塌陷行内尺寸包含生效,内容撑不开加显式宽度或允许断词
子元素高度变 0size类型的块向包含给容器或父级确定高度
字号在小屏极小上方没有可用容器,单位退化父级补container-type,并加clamp()下限
浮层位置偏移或被裁contain: layout改变包含块调整 DOM 层级或定位参照
修改后被覆盖层叠顺序问题调整声明顺序或用更具体的选择器

最后分享一个我一直在用的调试习惯。改容器查询之前,先在页面上临时加一条全局规则,把所有容器的边界画出来:

@supports (container-type: inline-size) { [style*="--debug-cq"] * { outline: 1px dashed rgba(255, 140, 0, 0.6); } }

更直接的办法是给每个容器加一个--cq-label自定义属性,配合伪元素把容器名和当前宽度打到角上。改造期间开着,改完立刻删掉。这一步花不了两分钟,但能省掉大量"我明明写了为什么不生效"的时间——因为大部分时候不是写法错了,而是你以为的那个容器,其实并不是元素实际匹配到的最近容器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询