返回首页

CSS Container Queries 实战:让组件根据容器自适应

媒体查询关心视口宽度,但组件真正需要关心的往往是自己所在容器的空间。本文从卡片、侧栏和网格布局出发,讲清楚 Container Queries 的使用方式、适用场景和常见边界。

47 约 6 分钟 · 5482 字 前端

传统响应式布局最常用的是媒体查询:

@media (min-width: 768px) {
  .card {
    display: grid;
    grid-template-columns: 160px 1fr;
  }
}

这类写法关心的是视口宽度。它适合页面级布局,但对组件来说经常不够精准。

同一个 ProductCard 可能出现在首页三栏网格里,也可能出现在侧栏推荐里。视口都是 1440px,但两个卡片实际拿到的宽度完全不同。如果只按视口判断,组件就很容易在某些容器里显得拥挤。

Container Queries 解决的就是这个问题:组件可以根据自己的容器宽度改变样式。

一句话定义

媒体查询问的是"浏览器窗口有多宽"。

Container Queries 问的是"我所在的容器有多宽"。

最小写法如下:

.card-shell {
  container-type: inline-size;
}

@container (min-width: 420px) {
  .product-card {
    display: grid;
    grid-template-columns: 140px 1fr;
  }
}

其中 .card-shell 是容器,.product-card 是容器里面被影响的元素。

inline-size 表示关注容器的行向尺寸。在常见横排页面里,你可以先简单理解为宽度。

为什么媒体查询不够

看一个常见页面:

<main class="layout">
  <section class="content">
    <ProductCard />
  </section>

  <aside class="sidebar">
    <ProductCard />
  </aside>
</main>

在桌面端,主内容区可能有 760px,侧栏只有 280px。它们处在同一个视口里,但卡片需要的样式并不一样。

如果用媒体查询,你只能写:

@media (min-width: 1024px) {
  .product-card {
    display: grid;
    grid-template-columns: 160px 1fr;
  }
}

问题是侧栏里的卡片也会变成横向布局,最后图片、标题、按钮挤在一起。

Container Queries 的思路更接近组件化:

.product-card-wrapper {
  container-type: inline-size;
}

@container (min-width: 420px) {
  .product-card {
    display: grid;
    grid-template-columns: 160px 1fr;
    align-items: start;
  }
}

主内容区的卡片够宽,就切成横向布局。侧栏卡片不够宽,就继续保持纵向布局。

基础例子:自适应卡片

HTML 结构可以保持很普通:

<article class="product-card-wrapper">
  <div class="product-card">
    <img class="product-card__cover" src="/demo.jpg" alt="产品预览图">

    <div class="product-card__body">
      <h2>机械键盘入门指南</h2>
      <p>从轴体、配列、键帽到连接方式,帮你避开第一把键盘的常见坑。</p>
      <button>查看详情</button>
    </div>
  </div>
</article>

默认样式先按窄容器设计:

.product-card-wrapper {
  container-type: inline-size;
}

.product-card {
  display: grid;
  gap: 12px;
}

.product-card__cover {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.product-card__body {
  display: grid;
  gap: 8px;
}

当容器空间足够时,再增强成横向布局:

@container (min-width: 420px) {
  .product-card {
    grid-template-columns: 160px 1fr;
  }

  .product-card__cover {
    height: 100%;
    aspect-ratio: 1;
  }
}

这套写法的好处是:组件默认适配窄空间,只在空间足够时增加复杂布局。

给容器命名

页面复杂以后,可能会有多个容器嵌套。为了让规则更明确,可以给容器命名:

.profile-panel {
  container-name: profile;
  container-type: inline-size;
}

@container profile (min-width: 560px) {
  .profile-card {
    grid-template-columns: auto 1fr auto;
  }
}

也可以合写:

.profile-panel {
  container: profile / inline-size;
}

命名之后,读代码的人能更快知道这条规则依赖哪个容器。

如果组件会被放进很多不同区域,建议在组件外层建立自己的容器。这样组件样式不需要猜父级页面结构。

Container Queries 和 CSS Grid 很搭

Container Queries 适合处理组件内部布局,CSS Grid 适合处理二维结构。两者组合起来很自然。

例如一个统计面板:

<section class="stats-panel">
  <div class="stats-grid">
    <article>访问量</article>
    <article>转化率</article>
    <article>客单价</article>
    <article>留存率</article>
  </div>
</section>

CSS:

.stats-panel {
  container: stats / inline-size;
}

.stats-grid {
  display: grid;
  gap: 12px;
  grid-template-columns: 1fr;
}

@container stats (min-width: 480px) {
  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@container stats (min-width: 760px) {
  .stats-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

这里的断点不再是整个页面的断点,而是统计面板自己的断点。

这让组件更容易复用。它被放进宽区域就展示四列,放进窄区域就自动回到一列或两列。

容器单位 cqw、cqh、cqi

Container Queries 还带来了一组容器查询单位。

常用的是:

  1. cqw:容器宽度的 1%
  2. cqh:容器高度的 1%
  3. cqi:容器行向尺寸的 1%
  4. cqb:容器块向尺寸的 1%

例如标题字号可以跟随容器宽度轻微变化:

.hero-card {
  container-type: inline-size;
}

.hero-card h2 {
  font-size: clamp(20px, 6cqi, 36px);
}

这比 vw 更适合组件。vw 依赖视口,组件放在侧栏里也会按整个窗口缩放,很容易过大。

不过要克制使用容器单位。字号、间距、图片尺寸可以适当响应容器,但不要让所有细节都动态变化,否则视觉会变得难以控制。

适合使用的场景

第一,复用型卡片。

商品卡、文章卡、用户卡、课程卡,经常出现在不同宽度的区域。它们非常适合根据容器切换布局。

第二,可拖拽或可配置面板。

仪表盘、低代码页面、后台工作台里,用户可能调整面板宽度。组件按自己的容器响应,比按视口响应自然得多。

第三,嵌入式模块。

同一个评论区、推荐列表、播放器控制条,可能被嵌入文章页、详情页、弹窗和侧栏。Container Queries 能减少页面层面的特殊覆盖。

第四,设计系统组件。

设计系统里的组件不应该强依赖某个页面断点。容器查询可以把响应式规则封装在组件内部。

不适合滥用的场景

页面整体布局仍然适合媒体查询。

比如顶部导航何时变成移动菜单、页面主栏和侧栏何时上下堆叠,这些通常是视口级决策。用媒体查询更直观。

不要为了使用 Container Queries,把每一层都设置成容器。容器会建立查询上下文,也可能影响某些布局计算。通常只需要给真正会影响内部布局的组件外层加容器。

也不要把业务状态塞进 CSS 断点。比如"会员用户展示更复杂布局",这是业务条件,不应该用容器宽度间接表达。

和 Tailwind 一起用

如果项目主要使用 Tailwind,可以把 Container Queries 作为组件 CSS 的补充。

一种务实做法是:布局基础类继续写在模板里,容器查询规则放到组件的 <style> 中。

<template>
  <article class="article-card-wrapper">
    <div class="article-card grid gap-3 rounded-lg border p-4">
      <img class="cover w-full object-cover" src="/cover.jpg" alt="">
      <div class="grid gap-2">
        <h2 class="text-lg font-semibold">文章标题</h2>
        <p class="text-sm text-slate-600">文章摘要...</p>
      </div>
    </div>
  </article>
</template>

<style scoped>
.article-card-wrapper {
  container: article-card / inline-size;
}

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

  .cover {
    height: 100%;
  }
}
</style>

这样不会把复杂断点全部塞进 class 字符串里,组件的响应式意图也更清楚。

常见坑

第一,忘记设置容器。

只有写了 container-typecontainer 的元素,才会成为查询容器。否则 @container 找不到可用容器,规则不会按预期生效。

第二,查询的是祖先容器,不是自己。

@container 影响的是容器里面的后代元素,不是容器元素本身。如果你要改变卡片根元素的布局,通常需要在外面再包一层容器。

第三,断点太多。

组件断点应该服务于明显的布局变化。不要每隔几十像素就写一条规则,维护成本会很高。

第四,和页面媒体查询职责混乱。

页面结构用媒体查询,组件内部用容器查询。这个分工能避免样式规则互相覆盖。

总结

Container Queries 让响应式设计从"页面视口"走向"组件容器"。

它最适合解决这样的问题:同一个组件在不同区域被复用,但实际可用宽度不同。

使用时记住三条原则:

  1. 默认先写窄容器样式
  2. 空间足够时再用 @container 增强
  3. 页面级布局继续交给媒体查询

这样写出来的组件更独立,也更符合现代前端的组件化思路。