用了五年媒体查询后,我决定把项目迁到容器查询——踩过的坑一次说清
用了五年媒体查询后,我决定把项目迁到容器查询——踩过的坑一次说清
去年我们把蹦熊的工单系统前端从媒体查询迁到了 CSS 容器查询。代码量少了一半,组件复用度高了一截,但过程没官方文档写得那么顺利。中间有几个坑翻得挺深,写下来给后面要迁的人当个参考。
为什么要迁
工单系统有一个左侧筛选栏 + 右侧详情区的主结构。详情区在不同业务场景下有四种宽度:
- 工单列表里的预览面板:300px
- 详情页整页:800px
- 全屏弹窗:1200px
- 报表里嵌入的小窗:400px
用媒体查询时代码是这样的:
```css
.detail-card {
display: flex;
flex-direction: column;
}
@media (min-width: 600px) {
.detail-card {
flex-direction: row;
}
}
@media (min-width: 900px) {
.detail-card {
padding: 24px;
font-size: 16px;
}
}
问题来了:同一个 .detail-card 组件,在 300px 宽的预览面板里,它爹容器是 300px,但视口宽度是 1920px。媒体查询看的是视口,不是父容器宽度。结果预览面板里的卡片用了横排布局,根本挤不下。
要么给每个场景写一套样式,要么用 JS 算容器宽度手动改 class。前者维护成本爆炸,后者性能不好。我们工单系统历史代码里这两种都存在,五花八门,看了脑仁疼。
容器查询的出现就是为了解决这个:让组件根据「我爹」而不是「视口」来响应。
基础用法,比想象中简单
CSS 容器查询的核心就两个 API:container-type 和 @container。
```css
.detail-card-wrapper {
container-type: inline-size;
container-name: card-area;
}
.detail-card {
display: flex;
flex-direction: column;
}
@container card-area (min-width: 600px) {
.detail-card {
flex-direction: row;
}
}
container-type: inline-size 告诉浏览器:这个元素会基于它的 inline 方向(默认横向)尺寸变化来触发容器查询。container-name 是给容器起个名字,多个容器嵌套时不冲突。
就这样,原本要写四个版本的样式,现在一个组件就搞定了。
坑一:浏览器兼容没你想的那么差,但也不完美
Can I Use 上的数据:2024 年中主流浏览器(Chrome 105+、Edge 105+、Safari 16+、Firefox 110+)都已经支持。我们工单系统的用户统计里,覆盖率达到了 96.5%。
剩下的 3.5% 主要是:
- 公司内网老机器上的 Chrome 90(管理员锁定不更新)
- 部分用户的 360 极速浏览器(Chromium 78 内核的旧版本)
- 极少数 Windows 7 上的 IE(这批用户我们已经不支持了)
我们的兜底策略是 @supports:
.detail-card-wrapper {
container-type: inline-size;
}
@supports not (container-type: inline-size) {
.detail-card-wrapper {
/ 退化到媒体查询 /
}
@media (min-width: 1200px) {
.detail-card {
flex-direction: row;
}
}
}
实际效果:3.5% 的用户看到的是基于视口的简化版,96.5% 看到的是正确的容器查询版。两边都不会崩,但正确的那一边明显更舒服。
## 坑二:`container-type` 会改变子元素的布局上下文
这一条是官方文档没怎么强调但实际会撞上的。
`container-type: inline-size` 让这个元素成为「containment context」,它的尺寸不再由子元素决定,而是由父容器决定。
听起来对,做起来容易翻车。我把一个 flex 容器改成 container-type,里面的子元素突然溢出、或者高度变成 0。原因:flex 容器默认是 `min-content` 计算高度,containment 改了 height 的解析方式。
解决:给容器明确的高度,或者改成 `display: grid`:
```css
.detail-card-wrapper {
container-type: inline-size;
height: 100%; /* 或者 min-height: 400px */
display: grid;
}
另外 `containment` 还有副作用:position 定位的参照系变了。如果组件内部用 `position: absolute` 相对自己定位的,containment 之后定位基准不变,但如果祖先链上有 `transform` 或 `filter`,会被剪掉。
## 坑三:嵌套查询的名字冲突
工单系统里卡片会嵌套卡片(外层工单卡,里字段卡)。两套都用了 `container-name`,结果查询条件搞混了。
调试时打印的样式表才知道,@container 默认是查「最近的」带名字的容器。如果你写的名字是别人已经用过的,最近的那个匹配上,远的就不响应了。
我们的做法是强制带名字:
.outer-card {
container-type: inline-size;
container-name: ticket-card;
}
.inner-card {
container-type: inline-size;
container-name: ticket-field;
}
@container ticket-card (min-width: 800px) { / 明确指定查外层 / }
@container ticket-field (min-width: 400px) { / 明确指定查内层 / }
规范上 @container 不带名字时,行为是「查最近的容器」,但嵌套场景下这个「最近」会让你怀疑人生,尤其是别人写的 CSS。
坑四:DevTools 的调试体验还在追
Chrome DevTools 在 110 之后才能在 Elements 面板看到容器查询的源映射。早期版本你 @container 写错名字,DevTools 不会告诉你,它就静默不生效,浏览器对不上的名字直接忽略。
Firefox 的容器查询调试比 Chrome 早做出来,但样式检查里显示得不直观。
我们开发时养成了几个习惯:
- 写完 @container 立刻在浏览器里缩放父容器,看是否生效
- 复杂场景加个临时的
outline: 1px solid red给容器,确认匹配范围 - 多个嵌套容器时把 container-name 写全
坑五:CSS-in-JS 和原子化框架的支持
我们项目用 Tailwind,Tailwind 3.4+ 已经有了 @container 工具类:
<div class="@container">
<div class="@md:flex-row flex flex-col">
<!-- 容器查询版的响应式 -->
</div>
</div>
但 Tailwind 的 `@md` 默认是 28rem (448px),如果你的业务断点不是这个,要改配置:
// tailwind.config.js
module.exports = {
theme: {
containers: {
sm: '400px',
md: '600px',
lg: '800px',
xl: '1200px',
},
},
}
CSS-in-JS(styled-components、Emotion)这边麻烦一些。容器查询的 @container 是 CSS 规则,写在 styled component 里需要用 css helper 包一层,IDE 提示容易丢。我同事有段时间改完代码保存,VSCode 没报错,运行时也不生效。最后是 & 选择器嵌套的问题。
```jsx
// 错误写法,&container 不在 styled component 的根上下文
const Wrapper = styled.div`
& {
container-type: inline-size;
}
@container (min-width: 600px) {
/* 这里写的样式属于全局,不属于 Wrapper */
}
`;
// 正确写法
const Wrapper = styled.div`
container-type: inline-size;
& .detail-card {
@container (min-width: 600px) {
flex-direction: row;
}
}
`;
坑六:动画和过渡在容器查询变化时不连续
容器查询触发的样式变化是没有 transition 的,因为浏览器认为这是「布局」变化不是「属性」变化。例如下面这个:
```css
.detail-card {
font-size: 14px;
transition: font-size 0.3s;
}
@container (min-width: 600px) {
.detail-card {
font-size: 18px;
}
}
你缩放父容器,font-size 会瞬间从 14px 跳到 18px,transition 不生效。原因是 @container 切换样式表是「yes/no」的状态切换,不是属性插值。
要平滑过渡,得用 JS 监听 ResizeObserver 算容器宽度,再动态改 CSS 变量。但这样就丢了容器查询的简洁性了。
我们的取舍:能接受跳变的就用 @container,需要过渡的用 JS 写。工单系统里 90% 的场景是「布局」变化(flex-direction、grid-template-columns),用户感知不到跳变;剩下 10% 的字号过渡需求单独处理。
迁完之后回头看,最大的收益不是代码量减少(虽然确实少了),而是组件终于能「自给自足」了。丢到任何容器里,它自己根据可用空间调整布局,不再依赖外部的视口假设。
新组件一律用容器查询。老组件看情况:纯展示的卡片、对话框内容区、报表嵌入块,优先迁;底层导航、全局 header 这种按视口断点的,保留媒体查询。
兼容性已经不成问题了,2026 年还在兼容 IE 11 的项目应该不多。如果你的项目用着 React 18 或 Vue 3 又被媒体查询的嵌套场景折磨过,可以考虑动手迁。