// PC530 技术论坛 v1.0
● online 登录 / 注册
返回列表
前端开发

用了五年媒体查询后,我决定把项目迁到容器查询——踩过的坑一次说清


用了五年媒体查询后,我决定把项目迁到容器查询——踩过的坑一次说清

去年我们把蹦熊的工单系统前端从媒体查询迁到了 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 早做出来,但样式检查里显示得不直观。

我们开发时养成了几个习惯:

  1. 写完 @container 立刻在浏览器里缩放父容器,看是否生效
  2. 复杂场景加个临时的 outline: 1px solid red 给容器,确认匹配范围
  3. 多个嵌套容器时把 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 又被媒体查询的嵌套场景折磨过,可以考虑动手迁。