CSS 容器查询实战:让组件真正自适应

同一个卡片组件,放在宽栏里横排很好看,塞进侧栏就挤成一团。问题的根源不是屏幕太窄,而是组件不知道自己有多宽。

一、媒体查询解决不了组件级适配

@media 查询的永远是视口宽度。桌面端视口 1440px,主内容区和侧边栏都处在同一个媒体查询断点下, 但它们的可用宽度可能相差三倍。为了让卡片在侧栏里也能正常显示,过去只能给组件加一堆 .is-narrow 之类的修饰类,由外部容器决定组件长什么样。

这违背了组件的封装原则:组件应该根据自己被放置的空间自动调整,而不是要求使用方事先知道该传什么类名。 容器查询(Container Queries)解决的正是这件事。

二、最小可用示例

使用容器查询分两步:先声明「谁是容器」,再在容器内部写 @container 规则。

/* 1. 声明容器,并告诉浏览器要查询哪个维度 */
.card-list {
  container-type: inline-size;
}

/* 2. 当最近的容器宽度 ≥ 480px 时生效 */
@container (min-width: 480px) {
  .card {
    display: grid;
    grid-template-columns: 148px 1fr;
    align-items: center;
  }
}

与媒体查询不同,@container 的条件针对的是最近的祖先容器, 而不是视口。所以只要把同一张卡片放进侧栏,它自然就会退回窄布局,完全不需要额外的类名。

三、container-type 到底在声明什么

取值 含义 适用场景
normal 不是查询容器(但仍可作为样式容器) 只想用容器单位
inline-size 按行内方向尺寸查询,通常就是宽度 绝大多数卡片、列表布局
size 同时按宽高查询 需要高度自适应的独立区域

日常开发中 inline-size 已经够用,而且它不会让容器在块方向上失去内容撑开的能力, 相比 size 更安全——后者会让容器在两个方向上都需要显式尺寸,容易出现高度塌陷。

四、给容器起名字

嵌套容器很常见:页面有布局容器,卡片列表自己也是容器。当 @container 未指定名称时, 它匹配的是最近的容器,这未必是你想要的那一个。此时可以为容器命名:

.layout {
  container: page / inline-size;
}

.card-list {
  container: cards / inline-size;
}

@container cards (min-width: 640px) {
  .card {
    grid-template-columns: 180px 1fr;
  }
}

container 是 container-name 与 container-type 的简写, 顺序是「名称 / 类型」。命名之后,规则就只会匹配对应名称的祖先容器,嵌套层级再深也不会误判。

五、完整实战:自适应卡片

下面是一份可以直接使用的实现,包含三种形态:窄屏纵向、中等横排、宽屏双列信息。

<ul class="card-list">
  <li class="card">
    <img class="card__cover" src="/cover.webp" alt="" />
    <div class="card__body">
      <h3 class="card__title">组件级响应式布局</h3>
      <p class="card__desc">根据容器宽度自动切换排布方式。</p>
    </div>
  </li>
</ul>
.card-list {
  container: cards / inline-size;
  display: grid;
  gap: 16px;
}

.card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 14px;
  background: #fff;
}

/* 容器达到 420px:图片与文字横排 */
@container cards (min-width: 420px) {
  .card {
    flex-direction: row;
  }
  .card__cover {
    width: 160px;
    flex-shrink: 0;
  }
}

/* 容器达到 720px:文字区域内部再分两列 */
@container cards (min-width: 720px) {
  .card__body {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 24px;
    padding: 20px 24px;
  }
  .card__desc {
    margin: 0;
  }
}

整个组件没有任何 @media,也不依赖外部传入的修饰类。 它被放进 300px 宽的侧栏就纵向排列,被放进 900px 宽的主区域就自动展开成横排。

六、容器单位:让字号跟着容器走

容器查询还带来了新的长度单位,最常用的是 cqw(容器宽度的 1%)与 cqi (行内方向的 1%)。它们让字号、间距可以相对于容器缩放:

.hero-title {
  font-size: clamp(20px, 4cqi, 40px);
}

.card__body {
  padding: 4cqi 5cqi;
}

需要注意,cqw 依赖于最近的查询容器。如果找不到任何容器,它会回退到视口单位, 行为上等同 vw,调试时容易误以为设置没生效。

七、与媒体查询协作,而不是取代它

两者职责不同,最佳实践是分层使用:

  • 页面级布局(导航折叠、栅格列数、侧栏是否显示)继续用 @media。
  • 组件内部排布(图片位置、文字列数、按钮大小)交给 @container。

这样组件在任意上下文中都能自洽,页面只需要关心整体结构,维护成本会明显下降。

八、兼容性与降级

容器查询在现代浏览器中已得到良好支持。若仍需照顾较旧的内核,可以用 @supports 做渐进增强,保证基础布局始终可用:

.card {
  display: flex;
  flex-direction: column;
}

@supports (container-type: inline-size) {
  .card-list {
    container: cards / inline-size;
  }

  @container cards (min-width: 420px) {
    .card {
      flex-direction: row;
    }
  }
}

没有容器查询支持的浏览器会停留在纵向布局,功能不受影响,只是视觉上少了一点自适应能力。

九、几个容易踩的坑

  • 容器不能查询自身。@container 只匹配祖先容器,给元素自己加 container-type 再查询自己是不生效的。
  • container-type 会创建包含块。它会让内部的绝对定位元素以容器为参照,同时影响尺寸计算,改造老项目时要留意。
  • 不要滥用 size。除非确实需要按高度适配,否则优先 inline-size。
  • 命名容器有助于可维护性。组件库尤其如此,避免被外层布局容器意外匹配。

十、小结

容器查询把「响应式」从页面级下沉到了组件级。它不会让媒体查询消失,但会让组件的边界更清晰: 组件只关心自己能拿到多少空间,页面只关心如何分配空间。这种职责划分,正是设计系统能够规模化的前提。