#React#TypeScript

CSS Container Queries:真正基于组件的响应式

Media Query 响应的是视口宽度,Container Query 响应的是父容器宽度——这让组件真正自适应(同一组件在侧边栏窄、主区宽各显示不同布局)。本文讲清 Container Queries 的原理、@container 语法、container-type 设置、浏览器支持现状,以及它怎么改变响应式设计的思路。

本文要点

  • 主题聚焦:React、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月4日·更新于 2026年7月23日

CSS Container Queries:真正基于组件的响应式

"Media Query 问的是'屏幕多宽',Container Query 问的是'我家多宽'。"

写过几年前端的开发者,多半有过这种体验:一个 UserCard 组件,放在主内容区是一张大方卡(头像 + 名字 + 简介 + 操作按钮四行),放到侧边栏里却挤成了一坨——头像被压缩、文字折行、按钮溢出。你对着它写 @media (max-width: 768px) 调小、调紧凑,但放到 Dashboard 的窄列里又不对了。

问题的根源不在你的 CSS,而在 Media Query 的响应对象错了:它响应的是浏览器视口(viewport)的宽度,而不是组件所在容器的宽度。同一个组件在同一个视口下,可能被塞进 200px 的侧栏,也可能被放进 800px 的主区——@media 完全无法区分这两种情况。

CSS Container Queries 是自 Flexbox、Grid 之后,响应式设计领域最大的一次范式跃迁。它让组件基于自己的容器做响应——不再关心视口,只关心"我现在被装在多大盒子里"。

这篇文章不讲 API 罗列,只讲原理 + 心智模型 + 实战取舍。读完你应该能:

  • 理解为什么 Media Query 在组件化时代注定力不从心
  • 掌握 container-type / @container / container-name 三件套
  • 写出"放之四海而皆准"的真正可复用组件
  • 知道 Container Query 与 Media Query 该如何分工协作
  • 看清从"页面响应式"到"组件响应式"的设计思路转变

一、Media Query 的局限:它响应的是视口,不是组件

要理解 Container Queries 解决了什么,先得看清 Media Query 的根本缺陷。

1.1 一个典型的"复用失败"场景

假设你有一个 ArticleCard 组件,在桌面视口下设计成横向布局(封面图在左、文字在右):

tsx
function ArticleCard({ article }: { article: Article }) {
  return (
    <article className="card">
      <img className="card-cover" src={article.cover} alt="" />
      <div className="card-body">
        <h3>{article.title}</h3>
        <p>{article.excerpt}</p>
      </div>
    </article>
  );
}

对应的 CSS(用 Media Query 写的"响应式"版本):

css
.card {
  display: flex;
  flex-direction: row;     /* 桌面默认横向 */
  gap: 16px;
}

/* 窄屏改成纵向 */
@media (max-width: 600px) {
  .card {
    flex-direction: column;
  }
}

看起来没问题。现在把它放进同一个页面的三个位置:

text
桌面视口 1440px
┌──────────────────────────────────────────────────────┐
│  Header                                              │
├──────────┬───────────────────────────────┬───────────┤
│          │  Main(800px 宽)             │ Sidebar   │
│  Narrow  │  ┌──────────────────────────┐ │ (240px)   │
│  Rail    │  │ ArticleCard              │ │ ┌───────┐ │
│  (160px) │  │ 横向,舒适 ✓             │ │ │Card   │ │
│  ┌────┐  │  └──────────────────────────┘ │ │横向?  │ │
│  │Card│  │                               │ │挤爆 ✗ │ │
│  │横向│  │                               │ └───────┘ │
│  │挤爆│  │                               │           │
│  └────┘  │                               │           │
└──────────┴───────────────────────────────┴───────────┘

视口是 1440px,所以 @media (max-width: 600px) 根本不触发——三张卡片全都走横向布局。结果:

  • 主区的卡片:800px 宽,横向布局舒适 ✓
  • 侧边栏的卡片:240px 宽,横向布局被挤成豆腐块 ✗
  • 窄 rail 的卡片:160px 宽,更惨 ✗

这就是 Media Query 的结构性缺陷:视口宽 ≠ 组件可用宽。在组件化、可复用的现代 UI 里,这俩经常严重背离。

1.2 "补丁式"方案为什么不可持续

为了绕过这个缺陷,开发者们发明了一堆补丁:

补丁 1:基于 prop 的变体

tsx
<Card layout="horizontal" />   {/* 主区用 */}
<Card layout="vertical" />     {/* 侧栏用 */}

问题:组件失去了"自适应"能力——调用方必须事先知道自己有多宽,然后手动选变体。但容器宽度可能随窗口缩放动态变化(比如可折叠侧栏),prop 是静态的,根本追不上。

补丁 2:用 ResizeObserver 在 JS 里监听

tsx
const ref = useRef<HTMLDivElement>(null);
const [narrow, setNarrow] = useState(false);

useEffect(() => {
  if (!ref.current) return;
  const ro = new ResizeObserver(([entry]) => {
    setNarrow(entry.contentRect.width < 400);
  });
  ro.observe(ref.current);
  return () => ro.disconnect();
}, []);

return <article ref={ref} className={narrow ? 'card card--vertical' : 'card'} />;

能跑,但代价巨大:

  • 每个 Card 实例一个 observer,列表里有 100 张卡就是 100 个 observer
  • JS 频繁触发重渲染,影响性能
  • 布局逻辑从 CSS 搬到了 JS,违反"样式归样式"的职责分离
  • SSR 场景下首屏拿不到尺寸,闪屏(FOUC)

补丁 3:Media Query 全家桶覆盖各种断点

css
@media (max-width: 600px) { ... }
@media (max-width: 900px) { ... }
@media (max-width: 1200px) { ... }

但只要组件复用到不同容器,这种"按视口猜容器"的做法注定漏掉某种组合。本质上,你在用错误的输入(视口宽度)做正确的决策(组件布局)。

1.3 根源:响应对象错了

把上面的所有问题浓缩成一句话:

Media Query 的响应对象是视口,但组件布局真正依赖的是容器宽度。这是一个根本性的语义错位。

Container Queries 把这个错位修正了——它让你直接对容器宽度做判断,而不是绕一圈去猜视口。


二、Container Queries 的价值:让组件自己决定自己

Container Queries 的核心价值,一句话就能讲清:

组件自己感知它所在容器的尺寸,并据此调整自己的布局——和视口无关,和调用方无关。

2.1 同一个组件,三种容器,三种布局

回到第一节那个 ArticleCard 的例子,用 Container Query 重写后:

css
.card-host {
  container-type: inline-size;   /* 声明容器 */
}

.card {
  display: flex;
  flex-direction: column;        /* 默认纵向 */
}

@container (min-width: 400px) {
  .card {
    flex-direction: row;         /* 容器 ≥ 400px 时改横向 */
  }
}

现在的效果:

text
桌面视口 1440px,同一组件放三处:

侧栏(240px)     主区(800px)       窄 rail(160px)
┌───────────┐    ┌──────────────┐    ┌─────────┐
│  [cover]  │    │[cover] 文字  │    │ [cover] │
│   标题    │    │       文字   │    │  标题   │
│   简介    │    │              │    │  简介   │
└───────────┘    └──────────────┘    └─────────┘
   纵向 ✓            横向 ✓            纵向 ✓

视口没变(1440px),三种容器三种布局,全部正确。 这就是 Container Query 想表达的东西。

2.2 真正的可复用组件

Container Query 给组件化带来的最大改变,是组件的响应式行为跟着组件本身走,而不是跟着页面走。

维度Media Query 时代Container Query 时代
响应对象浏览器视口组件所在的容器
复用性复用到不同位置可能要改 CSS同一个 CSS 在任何位置都自适应
调用方耦合调用方需要知道视口断点调用方完全不需要关心
适配 SSR天然支持天然支持(CSS 而非 JS)
心智模型"页面在小屏,改成单列""我所在盒子小,改成单列"

对于做设计系统(Design System) 的团队来说,这是革命性的——你的 Card、Modal、Form、NavItem 这些原子组件,可以真正做到"放进任何容器都长得对"。


三、container-type:声明一个容器

Container Query 的使用分两步:先声明容器,再查询容器。这一节讲声明。

3.1 为什么需要"声明"

很多人第一次用 Container Query 会踩这个坑——直接写 @container (min-width: 400px) { ... },发现什么效果都没有。原因是你没告诉浏览器哪个元素是容器。

Container Query 的工作机制是:

  1. 在某个元素上设置 container-type,让它成为"容器"
  2. 它的后代元素就能用 @container 查询这个容器的尺寸
  3. 注意:容器本身不能查询自己,只能查询它的祖先

所以"声明容器"是前提,否则 @container 不知道在查谁。

3.2 container-type: inline-size —— 99% 的场景用它

最常用的声明方式:

css
.sidebar {
  container-type: inline-size;
}

inline-size 的意思是:这个容器在 inline 方向(通常是宽度)上可以被后代查询。

为什么默认用 inline-size 而不是 size?因为大部分响应式布局关心的是宽度——"容器够不够宽,要不要换行、要不要改横向"。块方向(高度)通常由内容撑开,没必要查询。

实战中 99% 的 Container Query 场景,inline-size 就够了。

3.3 container-type: size —— 谨慎使用

css
.stage {
  container-type: size;
  height: 500px;   /* 必须显式设置高度! */
}

size 同时允许后代查询 inline 和 block 方向(即宽和高)。但有个硬性约束:元素本身必须有确定的大小(不能是 auto)。如果块方向是 auto(高度靠内容撑开),用 size 会让元素高度塌成 0,布局直接崩。

什么场景才需要 size?通常是固定尺寸的画布类容器:地图、可视化面板、可缩放的舞台。日常业务里的列表、卡片、表单,几乎都用不到。

3.4 containment:Container Query 的代价

声明 container-type 之后,浏览器会给该元素加上 containment( containment),本质上是承诺:"这个元素的子树不会影响外部的布局和尺寸。"

这有两个含义:

  1. 性能收益:子树布局变化被隔离在容器内部,浏览器不用重新计算外部布局——这是 Container Query 在性能上优于 ResizeObserver 的根本原因。
  2. 副作用:容器的块方向(高度)不再由内容撑开(inline-size 模式下,块方向仍是 auto,没事;size 模式下则必须是显式高度)。

理解 containment 的存在,能解释很多"为什么这个容器的高度表现怪怪的"的疑问。

3.5 容器 shorthand:一行写完

css
.sidebar {
  container: sidebar / inline-size;
}

等价于:

css
.sidebar {
  container-name: sidebar;
  container-type: inline-size;
}

container: <name> / <type> 是简写形式,名字和类型用 / 分隔。实际项目里这种写法最常见。


四、@container:在容器内查询

声明完容器,后代就能用 @container 查询了。语法和 @media 几乎一致,迁移成本极低。

4.1 最基本的查询

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

含义:最近的祖先容器宽度 ≥ 400px 时,应用花括号里的样式。

注意几个点:

  • 查询的是最近的祖先容器——没显式命名的话,浏览器会自动找到最近的 container-type 祖先
  • 条件用的还是 min-width / max-width,但语义已经变了:这里是容器宽度,不是视口宽度
  • 写法、嵌套规则、优先级都和 @media 一致

4.2 范围查询(Range Syntax)

现代 CSS 支持范围语法,写起来更直观:

css
/* 老写法 */
@container (min-width: 400px) and (max-width: 800px) {
  .card { padding: 16px; }
}

/* 新的范围写法 */
@container (400px <= width <= 800px) {
  .card { padding: 16px; }
}

width <= 800px 比 max-width: 800px 易读,且不容易写错边界。这是 CSS Values Level 4 的特性,主流浏览器都支持。

4.3 容器查询单位:cqw / cqi / cqh / cqb / cqmin / cqmax

Container Query 还配套了一组新单位,类似 vw / vh 但相对于容器:

单位含义类比
cqw容器宽度的 1%vw
cqh容器高度的 1%(需 size 容器)vh
cqi容器 inline 方向的 1%(推荐用)vi
cqb容器 block 方向的 1%vb
cqmincqi 和 cqb 中较小者vmin
cqmaxcqi 和 cqb 中较大者vmax

推荐用 cqi 而不是 cqw——它自动适配书写模式(writing-mode: vertical-rl 时会指向高度),是逻辑属性,更稳健。

一个实战场景:字号随容器宽度缩放,组件放大时字号也变大,不再需要写一堆断点:

css
.tile-host { container-type: inline-size; }

.tile {
  /* 基础 1rem,每 100cqi 多 0.1rem,最大 2rem */
  font-size: clamp(1rem, 5cqi + 0.5rem, 2rem);
}

clamp() + cqi 是个被低估的组合:连续、平滑、无需断点。容器越宽,字号越大,但封顶在 2rem——既不会被无限放大,也不会在小容器里挤成蚂蚁。

4.4 一个常见的坑:容器查询不到自己

css
/* 错误:card 想查询自己,但找不到容器 */
.card {
  container-type: inline-size;   /* card 是容器 */
}

@container (min-width: 400px) {
  .card {
    /* 永远不会触发——card 不能查询自己 */
    flex-direction: row;
  }
}

@container 查询的是祖先容器。如果你想基于自己的宽度调整内部布局,正确做法是再加一层 wrapper:

css
.card-host {
  container-type: inline-size;   /* host 是容器 */
}

.card {
  /* 默认纵向 */
  flex-direction: column;
}

@container (min-width: 400px) {
  .card {                        /* card 是 host 的后代,查询生效 */
    flex-direction: row;
  }
}
tsx
<div className="card-host">      {/* 容器层 */}
  <article className="card">     {/* 查询层 */}
    ...
  </article>
</div>

两层层级(容器 / 内容)是 Container Query 的标准结构,初学者一定要记住。


五、container-name:命名容器(嵌套场景的救星)

随着项目变复杂,容器会嵌套——一个容器套着另一个容器,里面又有一个容器。这时 @container 默认查最近的祖先可能不是你想要的。container-name 解决这个精确指向的问题。

5.1 嵌套容器的歧义

假设你有这样的结构:

html
<div class="layout">        <!-- 主布局容器 -->
  <div class="panel">       <!-- 面板容器 -->
    <div class="card">...</div>
  </div>
</div>
css
.layout { container-type: inline-size; }
.panel  { container-type: inline-size; }

@container (min-width: 600px) {
  .card { /* 这里查的是 .panel,因为它最近 */ }
}

如果你想让 .card 根据 .layout(而非 .panel)的尺寸响应,默认行为就力不从心了。你需要给容器起名字。

5.2 命名容器的写法

css
.layout {
  container: layout / inline-size;   /* 命名为 layout */
}

.panel {
  container: panel / inline-size;    /* 命名为 panel */
}

/* 查询名为 layout 的容器 */
@container layout (min-width: 600px) {
  .card {
    background: hotpink;
  }
}

/* 查询名为 panel 的容器 */
@container panel (min-width: 400px) {
  .card {
    padding: 24px;
  }
}

语法:@container <name> (<condition>)——名字放在条件前。

5.3 什么时候需要命名

经验法则:

  • 单层容器:不命名也无所谓,默认查最近祖先就好
  • 多层嵌套:强烈建议命名,否则后期维护时没人知道 @container 查的是哪一层
  • 设计系统组件:必须命名,否则外部容器会"污染"组件内部的查询

设计系统里的最佳实践:组件内部的容器用组件前缀命名,避免和外层容器的名字冲突:

css
/* 设计系统内部 */
.ds-card {
  container: ds-card-host / inline-size;
}

@container ds-card-host (min-width: 400px) {
  .ds-card__inner { ... }
}

这样无论 ds-card 被谁用、外层有没有别的容器,组件内部的查询永远指向自己的 host——这是写可复用组件时的关键习惯。

5.4 一个真实的嵌套例子

考虑一个 Dashboard:外层是页面布局容器,里头是卡片面板容器,再里头是图表组件:

css
.dashboard {
  container: dashboard / inline-size;
}

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

.chart { /* 默认样式 */ }

/* 页面布局变化:图表背景色 */
@container dashboard (min-width: 1200px) {
  .chart {
    background: var(--bg-wide);
  }
}

/* 面板尺寸变化:图表是否显示图例 */
@container panel (min-width: 400px) {
  .chart__legend {
    display: block;
  }
}

@container panel (max-width: 399px) {
  .chart__legend {
    display: none;   /* 窄面板隐藏图例 */
  }
}

每一层容器都有明确职责,命名让代码"自解释"——任何人读到 @container panel (...) 都知道这是面板级别的响应。没有命名的嵌套 Container Query 是未来维护的灾难,这点怎么强调都不过分。


六、浏览器支持:2023 起全主流支持,可放心用

讨论任何新 CSS 特性都绕不开"能用了吗"。Container Query 这方面已经完全没有顾虑。

6.1 支持现状

  • Chrome / Edge 105+:2022 年 8 月起支持
  • Safari 16+:2022 年 9 月起支持
  • Firefox 110+:2023 年 2 月起支持
  • 所有主流浏览器:自 2023 年起全部支持,覆盖率 95%+(来源:caniuse)

Container Queries 已于 2023 年进入 Baseline(W3C 与各大浏览器厂商共同认可的"可放心用于生产"标准)。也就是说:

2026 年的今天,Container Query 是一项完全成熟、零顾虑可用的 CSS 特性。

它已经不像几年前的 Grid 那样需要加 -webkit- 前缀或写 fallback——直接用就行。

6.2 渐进增强:@supports 兜底

如果你的项目还要兼容极老浏览器(比如某些政企客户的 IE 套壳),可以用 @supports 做渐进增强:

css
/* 默认样式(老浏览器看到这个) */
.card {
  flex-direction: column;
}

/* 支持 container-type 的浏览器额外应用 */
@supports (container-type: inline-size) {
  .card-host {
    container-type: inline-size;
  }

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

/* 不支持时退回 Media Query */
@supports not (container-type: inline-size) {
  @media (min-width: 768px) {
    .card { flex-direction: row; }
  }
}

但这种 polyfill 思路现在基本不需要了——主流项目的 baseline 都已经过 IE 时代。优先直接用 Container Query,不要给自己加无谓的兼容包袱。

6.3 Polyfill 的取舍

社区有 container-query-polyfill 这类 JS 方案,但我强烈不建议在生产里用:

  • JS polyfill 走 ResizeObserver 路径,性能比原生差一截
  • 首屏必然闪屏(FOUC)
  • 维护活跃度低,bug 多

正确的做法是:直接用原生 Container Query,不支持就降级到基础布局。2026 年了,没人在乎那 2% 的老浏览器用户看到的是横向卡片还是纵向卡片。


七、实战:同一个卡片组件,窄容器单列 / 宽容器双列

理论讲够了,来一个完整的实战——这是我在团队里推广 Container Query 时最爱用的例子。

7.1 需求

设计稿给了一张 ProductCard:

  • 容器宽 ≥ 480px:横向双列(图在左、信息在右)
  • 容器宽 280–480px:纵向单列(图在上、信息在下)
  • 容器宽 < 280px:紧凑纵向(图缩小、信息精简)

这个卡片要在三个地方复用:商品列表(宽容器)、相关推荐(中容器)、购物车侧栏(窄容器)。要求:组件本身不知道自己被放哪。

7.2 组件实现

tsx
// ProductCard.tsx
import './ProductCard.css';

interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  rating: number;
}

export function ProductCard({ product }: { product: Product }) {
  return (
    // host 层:作为容器
    <div className="pc-host">
      {/* inner 层:响应容器 */}
      <article className="pc">
        <img className="pc__image" src={product.image} alt={product.name} />
        <div className="pc__info">
          <h3 className="pc__name">{product.name}</h3>
          <div className="pc__meta">
            <span className="pc__rating">★ {product.rating}</span>
            <span className="pc__price">¥{product.price}</span>
          </div>
          <button className="pc__cta">加入购物车</button>
        </div>
      </article>
    </div>
  );
}
css
/* ProductCard.css */

/* 1. 声明容器(命名,避免外部污染) */
.pc-host {
  container: pc-host / inline-size;
}

/* 2. 默认(最窄情况):紧凑纵向 */
.pc {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
}

.pc__image {
  width: 100%;
  height: 80px;
  object-fit: cover;
  border-radius: 4px;
}

.pc__name {
  font-size: 0.875rem;
  margin: 0;
}

.pc__meta {
  display: flex;
  justify-content: space-between;
  font-size: 0.75rem;
}

.pc__cta {
  font-size: 0.75rem;
  padding: 4px 8px;
}

/* 3. 中等容器(≥ 280px):标准纵向,图变大 */
@container pc-host (min-width: 280px) {
  .pc {
    gap: 12px;
    padding: 12px;
  }

  .pc__image {
    height: 160px;
  }

  .pc__name {
    font-size: 1rem;
  }

  .pc__meta {
    font-size: 0.875rem;
  }

  .pc__cta {
    font-size: 0.875rem;
    padding: 8px 16px;
  }
}

/* 4. 宽容器(≥ 480px):横向双列 */
@container pc-host (min-width: 480px) {
  .pc {
    flex-direction: row;
    align-items: center;
    gap: 16px;
    padding: 16px;
  }

  .pc__image {
    width: 140px;
    height: 140px;
    flex-shrink: 0;
  }

  .pc__info {
    flex: 1;
  }

  .pc__name {
    font-size: 1.125rem;
  }
}

7.3 三处复用,三个布局

tsx
// 商品列表:宽容器(每张卡片 ≥ 480px)
<section className="product-grid">
  {products.map(p => <ProductCard key={p.id} product={p} />)}
</section>

// 相关推荐:中容器(约 320px)
<aside className="related-rail">
  {related.map(p => <ProductCard key={p.id} product={p} />)}
</aside>

// 购物车侧栏:窄容器(约 200px)
<div className="cart-sidebar">
  {cart.map(p => <ProductCard key={p.id} product={p} />)}
</div>

效果:

text
商品列表(容器 600px)           相关推荐(容器 320px)
┌─────────────────────────┐     ┌──────────────────┐
│ ┌─────┐                 │     │   ┌──────────┐   │
│ │     │  商品名         │     │   │          │   │
│ │ img │  ★ 4.5  ¥99     │     │   │   img    │   │
│ │     │  [加入购物车]   │     │   │          │   │
│ └─────┘                 │     │   └──────────┘   │
└─────────────────────────┘     │   商品名         │
   横向双列 ✓                    │   ★ 4.5  ¥99     │
                                │   [加入购物车]   │
                                └──────────────────┘
                                   纵向标准 ✓

购物车侧栏(容器 200px)
┌────────────────┐
│ ┌────────────┐ │
│ │    img     │ │
│ └────────────┘ │
│ 商品名         │
│ ★ 4.5 ¥99      │
│ [加入]         │
└────────────────┘
   紧凑纵向 ✓

同一个组件,三个容器,三种布局,零 prop,零 JS。 这就是 Container Query 的威力。

7.4 这个模式的可维护性优势

把这个例子和"prop 变体"或"ResizeObserver"方案对比:

维度prop 变体ResizeObserverContainer Query
组件 API多个 layout prop不变不变
性能优差(每实例一 observer)优(原生 containment)
SSR 友好是否(首屏闪屏)是
容器变化响应否(静态)是是
调用方负担高(要知道用哪个)中零

Container Query 在每一项都胜出或持平。 对于这种"组件需要在多种容器中复用"的场景,它就是当下最优解。


八、与 Media Query 配合:各司其职

Container Query 不是来"取代"Media Query 的——它们解决不同层级的问题。成熟的用法是让两者各司其职。

8.1 一个清晰的分工原则

页面级布局用 Media Query,组件级布局用 Container Query。

具体说:

  • Media Query 适合:整个页面的结构变化(侧栏显隐、栅格列数、header 高度)。这些变化本质上是视口驱动的——你的页面就是装在视口里。
  • Container Query 适合:组件内部的布局变化(卡片横纵、信息密度、CTA 文案)。这些变化本质上是容器驱动的——组件只关心自己的家有多大。

8.2 一个完整的页面例子

tsx
<div className="page">
  <header className="page__header" />
  <aside className="page__sidebar" />
  <main className="page__main">
    <ProductList />
  </main>
</div>
css
/* ===== 页面级:Media Query 控制整体布局 ===== */
.page {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas:
    "header"
    "main"
    "sidebar";
}

.page__header  { grid-area: header; }
.page__main    { grid-area: main; }
.page__sidebar { grid-area: sidebar; }

/* 视口够宽,开侧栏 */
@media (min-width: 768px) {
  .page {
    grid-template-columns: 240px 1fr;
    grid-template-areas:
      "header  header"
      "sidebar main";
  }
}

/* ===== 组件级:Container Query 控制卡片内部 ===== */
.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px;
}

.pc-host {
  container: pc-host / inline-size;   /* 每张卡片自己的容器 */
}

@container pc-host (min-width: 480px) {
  .pc { flex-direction: row; }
}

注意这里的设计意图:

  • 页面整体是否两栏——看视口(Media Query)
  • 每张卡片是否横向——看它所在的网格单元宽度(Container Query)

两者完全正交,互不干扰。Media Query 决定页面有多少空间给主区,主区里的 auto-fit + minmax 决定每张卡片占多大格子,最后 Container Query 决定卡片内部怎么排——三层各管一段,干净利落。

8.3 一个反模式:用 Container Query 取代所有 Media Query

有些团队初次接触 Container Query 后会陷入"全 Container"狂热——给 body 加 container-type: inline-size,把所有 @media 改成 @container。这没必要,而且有些场景 Container Query 反而不如 Media Query:

  • 字体大小随视口变化(html { font-size: ... }):根级响应就该用 Media Query
  • 全局主题切换(暗色模式、移动端隐藏导航):和容器无关,是视口/设备维度
  • 打印样式(@media print):根本没有"容器"概念

记住:Container Query 是组件化时代的补充,不是 Media Query 的替代品。把它们当成两件工具,根据响应对象选择。


九、设计思路的转变:从"页面响应式"到"组件响应式"

技术只是表象,Container Query 真正改变的是响应式设计的心智模型。这一节讲思路。

9.1 单元查询(Media Query)vs 容器查询(Container Query)

用术语来区分:

  • Media Query 是"单元查询"(Viewport Query):它查询的是浏览器的"单元"——视口。一切的判断基于"这个页面整体多大"。
  • Container Query 是"容器查询":它查询的是某个具体元素的盒子。判断基于"我家多大"。

这种区分不只是技术上的,更是设计哲学上的:

维度单元查询(Media)容器查询(Container)
思考起点"屏幕多大?""这个盒子多大?"
设计粒度整页单个组件
组件耦合组件依赖外部环境组件自洽
适合架构页面驱动(Page-driven)组件驱动(Component-driven)
历史背景文档为中心的 Web 1.0组件化框架(React/Vue)时代

9.2 组件化时代的必然选择

为什么 Container Query 在 2023 年才全面落地,而不是 2013 年?

因为 2013 年的 Web 还是以页面为单位——一个 URL 一张页面,组件化程度低,Media Query 完全够用。但 React/Vue 兴起后,UI 变成了组件树,一个组件可能在任意页面、任意位置被复用。这时 Media Query 的"视口中心论"就显得格格不入。

Container Query 是组件化 UI 的必然产物。 它把响应式的能力从"页面"下放到了"组件"——和组件化框架的设计哲学完全一致。

9.3 设计系统的范式转变

做 Design System 的团队,Container Query 带来的改变是根本性的:

旧范式:组件提供一组 prop(size="sm" | "md" | "lg"),调用方根据"自己大概多宽"选一个。问题是调用方经常选错,且窗口缩放时 prop 不会变。

新范式:组件自己用 Container Query 感知容器,调用方完全不操心。组件在不同容器里自动呈现最合适的形态。

这意味着:

  • 组件的接口变窄了(少了一堆 layout 相关的 prop)
  • 组件的鲁棒性变强了(不会因为放错位置而崩)
  • 设计系统的文档变简单了(不用写"在侧栏用 size=sm,在主区用 size=md"这种说明书)

9.4 一个观念的转变

最后,讲一个我常跟团队强调的观念:

响应式不是"页面在不同屏幕下长什么样",而是"组件在不同容器里长什么样"。

前者是页面思维,后者是组件思维。Container Query 让我们第一次有能力在 CSS 里精确表达后者——这是它最大的价值,远超语法本身的便利。

当你开始用"组件会出现在哪些容器里"来思考响应式,而不是"页面在哪些断点下变化",你的 CSS 架构会自动向更解耦、更可复用、更好维护的方向演进。


小结

Container Queries 是 CSS 响应式设计的真正革命。把它和 Media Query、Grid、Flexbox 放在一起,你已经掌握了现代 CSS 布局的全部主力武器。三条要点拎清楚:

  1. Media Query 的根本缺陷是响应对象错位——它响应视口,但组件布局依赖的是容器宽度。 这导致组件复用到不同容器(侧栏 vs 主区 vs 弹窗)时 Media Query 注定力不从心。Container Query 直接修正这个错位:组件基于自己的容器做响应。

  2. 三件套:container-type 声明容器、@container 查询容器、container-name 命名容器。 实战心法:99% 的场景用 container-type: inline-size;查询永远查祖先容器(所以用"host + 内容"两层层级结构);多层嵌套和设计系统组件必须命名,否则后期维护是灾难。

  3. Container Query 与 Media Query 不是替代关系,而是分工关系。 页面级布局(侧栏显隐、栅格列数)用 Media Query——它本质是视口驱动的;组件级布局(卡片横纵、信息密度)用 Container Query——它本质是容器驱动的。各司其职,两层正交。

3 条实战经验:

  • 2026 年了,直接用原生 Container Query,不要 polyfill。 主流浏览器自 2023 年起全面支持,覆盖率 95%+,已进 Baseline。container-query-polyfill 性能差、首屏闪屏,没必要。
  • clamp() + cqi 是无断点缩放的神器。 字号、间距、padding 跟着容器连续变化,不再为每个尺寸写断点。记得封顶,否则大容器里会爆。
  • 设计系统里的容器一定要用组件前缀命名(如 ds-card-host)。 否则外部容器的 container-type 会"污染"组件内部查询——这是可复用组件最常见的隐形 bug。

金句:Media Query 让页面适应屏幕,Container Query 让组件适应容器。前者是文档时代的遗产,后者是组件时代的答案——当你开始用"容器"而非"视口"思考响应式,你的组件才真正属于它自己。

下一篇我们会聊 现代 CSS 的逻辑属性与书写模式——margin-inline、padding-block 这些"反直觉"的新属性,为什么才是国际化与可维护布局的正确起点。

Related

相关推荐