CSS Container Queries:真正基于组件的响应式
Media Query 响应的是视口宽度,Container Query 响应的是父容器宽度——这让组件真正自适应(同一组件在侧边栏窄、主区宽各显示不同布局)。本文讲清 Container Queries 的原理、@container 语法、container-type 设置、浏览器支持现状,以及它怎么改变响应式设计的思路。
本文要点
- 主题聚焦:React、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
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 组件,在桌面视口下设计成横向布局(封面图在左、文字在右):
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 写的"响应式"版本):
.card {
display: flex;
flex-direction: row; /* 桌面默认横向 */
gap: 16px;
}
/* 窄屏改成纵向 */
@media (max-width: 600px) {
.card {
flex-direction: column;
}
}
看起来没问题。现在把它放进同一个页面的三个位置:
桌面视口 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 的变体
<Card layout="horizontal" /> {/* 主区用 */}
<Card layout="vertical" /> {/* 侧栏用 */}
问题:组件失去了"自适应"能力——调用方必须事先知道自己有多宽,然后手动选变体。但容器宽度可能随窗口缩放动态变化(比如可折叠侧栏),prop 是静态的,根本追不上。
补丁 2:用 ResizeObserver 在 JS 里监听
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 全家桶覆盖各种断点
@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 重写后:
.card-host {
container-type: inline-size; /* 声明容器 */
}
.card {
display: flex;
flex-direction: column; /* 默认纵向 */
}
@container (min-width: 400px) {
.card {
flex-direction: row; /* 容器 ≥ 400px 时改横向 */
}
}
现在的效果:
桌面视口 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 的工作机制是:
- 在某个元素上设置
container-type,让它成为"容器" - 它的后代元素就能用
@container查询这个容器的尺寸 - 注意:容器本身不能查询自己,只能查询它的祖先
所以"声明容器"是前提,否则 @container 不知道在查谁。
3.2 container-type: inline-size —— 99% 的场景用它
最常用的声明方式:
.sidebar {
container-type: inline-size;
}
inline-size 的意思是:这个容器在 inline 方向(通常是宽度)上可以被后代查询。
为什么默认用 inline-size 而不是 size?因为大部分响应式布局关心的是宽度——"容器够不够宽,要不要换行、要不要改横向"。块方向(高度)通常由内容撑开,没必要查询。
实战中 99% 的 Container Query 场景,inline-size 就够了。
3.3 container-type: size —— 谨慎使用
.stage {
container-type: size;
height: 500px; /* 必须显式设置高度! */
}
size 同时允许后代查询 inline 和 block 方向(即宽和高)。但有个硬性约束:元素本身必须有确定的大小(不能是 auto)。如果块方向是 auto(高度靠内容撑开),用 size 会让元素高度塌成 0,布局直接崩。
什么场景才需要 size?通常是固定尺寸的画布类容器:地图、可视化面板、可缩放的舞台。日常业务里的列表、卡片、表单,几乎都用不到。
3.4 containment:Container Query 的代价
声明 container-type 之后,浏览器会给该元素加上 containment( containment),本质上是承诺:"这个元素的子树不会影响外部的布局和尺寸。"
这有两个含义:
- 性能收益:子树布局变化被隔离在容器内部,浏览器不用重新计算外部布局——这是 Container Query 在性能上优于 ResizeObserver 的根本原因。
- 副作用:容器的块方向(高度)不再由内容撑开(
inline-size模式下,块方向仍是auto,没事;size模式下则必须是显式高度)。
理解 containment 的存在,能解释很多"为什么这个容器的高度表现怪怪的"的疑问。
3.5 容器 shorthand:一行写完
.sidebar {
container: sidebar / inline-size;
}
等价于:
.sidebar {
container-name: sidebar;
container-type: inline-size;
}
container: <name> / <type> 是简写形式,名字和类型用 / 分隔。实际项目里这种写法最常见。
四、@container:在容器内查询
声明完容器,后代就能用 @container 查询了。语法和 @media 几乎一致,迁移成本极低。
4.1 最基本的查询
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
含义:最近的祖先容器宽度 ≥ 400px 时,应用花括号里的样式。
注意几个点:
- 查询的是最近的祖先容器——没显式命名的话,浏览器会自动找到最近的
container-type祖先 - 条件用的还是
min-width/max-width,但语义已经变了:这里是容器宽度,不是视口宽度 - 写法、嵌套规则、优先级都和
@media一致
4.2 范围查询(Range Syntax)
现代 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 |
cqmin | cqi 和 cqb 中较小者 | vmin |
cqmax | cqi 和 cqb 中较大者 | vmax |
推荐用 cqi 而不是 cqw——它自动适配书写模式(writing-mode: vertical-rl 时会指向高度),是逻辑属性,更稳健。
一个实战场景:字号随容器宽度缩放,组件放大时字号也变大,不再需要写一堆断点:
.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 一个常见的坑:容器查询不到自己
/* 错误:card 想查询自己,但找不到容器 */
.card {
container-type: inline-size; /* card 是容器 */
}
@container (min-width: 400px) {
.card {
/* 永远不会触发——card 不能查询自己 */
flex-direction: row;
}
}
@container 查询的是祖先容器。如果你想基于自己的宽度调整内部布局,正确做法是再加一层 wrapper:
.card-host {
container-type: inline-size; /* host 是容器 */
}
.card {
/* 默认纵向 */
flex-direction: column;
}
@container (min-width: 400px) {
.card { /* card 是 host 的后代,查询生效 */
flex-direction: row;
}
}
<div className="card-host"> {/* 容器层 */}
<article className="card"> {/* 查询层 */}
...
</article>
</div>
两层层级(容器 / 内容)是 Container Query 的标准结构,初学者一定要记住。
五、container-name:命名容器(嵌套场景的救星)
随着项目变复杂,容器会嵌套——一个容器套着另一个容器,里面又有一个容器。这时 @container 默认查最近的祖先可能不是你想要的。container-name 解决这个精确指向的问题。
5.1 嵌套容器的歧义
假设你有这样的结构:
<div class="layout"> <!-- 主布局容器 -->
<div class="panel"> <!-- 面板容器 -->
<div class="card">...</div>
</div>
</div>
.layout { container-type: inline-size; }
.panel { container-type: inline-size; }
@container (min-width: 600px) {
.card { /* 这里查的是 .panel,因为它最近 */ }
}
如果你想让 .card 根据 .layout(而非 .panel)的尺寸响应,默认行为就力不从心了。你需要给容器起名字。
5.2 命名容器的写法
.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查的是哪一层 - 设计系统组件:必须命名,否则外部容器会"污染"组件内部的查询
设计系统里的最佳实践:组件内部的容器用组件前缀命名,避免和外层容器的名字冲突:
/* 设计系统内部 */
.ds-card {
container: ds-card-host / inline-size;
}
@container ds-card-host (min-width: 400px) {
.ds-card__inner { ... }
}
这样无论 ds-card 被谁用、外层有没有别的容器,组件内部的查询永远指向自己的 host——这是写可复用组件时的关键习惯。
5.4 一个真实的嵌套例子
考虑一个 Dashboard:外层是页面布局容器,里头是卡片面板容器,再里头是图表组件:
.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 做渐进增强:
/* 默认样式(老浏览器看到这个) */
.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 组件实现
// 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>
);
}
/* 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 三处复用,三个布局
// 商品列表:宽容器(每张卡片 ≥ 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>
效果:
商品列表(容器 600px) 相关推荐(容器 320px)
┌─────────────────────────┐ ┌──────────────────┐
│ ┌─────┐ │ │ ┌──────────┐ │
│ │ │ 商品名 │ │ │ │ │
│ │ img │ ★ 4.5 ¥99 │ │ │ img │ │
│ │ │ [加入购物车] │ │ │ │ │
│ └─────┘ │ │ └──────────┘ │
└─────────────────────────┘ │ 商品名 │
横向双列 ✓ │ ★ 4.5 ¥99 │
│ [加入购物车] │
└──────────────────┘
纵向标准 ✓
购物车侧栏(容器 200px)
┌────────────────┐
│ ┌────────────┐ │
│ │ img │ │
│ └────────────┘ │
│ 商品名 │
│ ★ 4.5 ¥99 │
│ [加入] │
└────────────────┘
紧凑纵向 ✓
同一个组件,三个容器,三种布局,零 prop,零 JS。 这就是 Container Query 的威力。
7.4 这个模式的可维护性优势
把这个例子和"prop 变体"或"ResizeObserver"方案对比:
| 维度 | prop 变体 | ResizeObserver | Container 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 一个完整的页面例子
<div className="page">
<header className="page__header" />
<aside className="page__sidebar" />
<main className="page__main">
<ProductList />
</main>
</div>
/* ===== 页面级: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 布局的全部主力武器。三条要点拎清楚:
-
Media Query 的根本缺陷是响应对象错位——它响应视口,但组件布局依赖的是容器宽度。 这导致组件复用到不同容器(侧栏 vs 主区 vs 弹窗)时 Media Query 注定力不从心。Container Query 直接修正这个错位:组件基于自己的容器做响应。
-
三件套:
container-type声明容器、@container查询容器、container-name命名容器。 实战心法:99% 的场景用container-type: inline-size;查询永远查祖先容器(所以用"host + 内容"两层层级结构);多层嵌套和设计系统组件必须命名,否则后期维护是灾难。 -
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