容器查询应用

张玥 2026年9月18日 阅读时间 45分钟
容器查询 响应式设计 组件级响应 CSS Containment 渐进增强
CSS高级技巧响应式设计之容器查询应用

过去十多年里,我们做响应式设计几乎只有一件武器:媒体查询。它盯着视口宽度,把整张页面切成几档。可当页面被拆成一个个可复用的组件之后,问题就来了——同一个卡片组件,放在侧边栏里只有 280px,放在主内容区有 700px,它凭什么要用同一套样式?媒体查询看不到“组件自己有多宽”,它只关心“浏览器窗口有多宽”。容器查询(Container Queries)彻底改变了这件事:组件第一次可以感知自己的容器尺寸,并据此调整内部布局。这不是媒体查询的替代品,而是响应式设计从“页面级”走向“组件级”的分水岭。这篇 45 分钟长文,从语法、容器类型、容器单位,一路讲到卡片、侧边栏、表格三大实战,再到 style()、scroll-state()、命名嵌套、性能陷阱与兼容策略,并配有多个可实时调节的交互实验台,帮你把容器查询真正用进项目里。

为什么媒体查询不够用:组件复用的困境

先看一个真实场景。你写了一个漂亮的 .product-card,包含缩略图、标题、描述和标签。在首页的主内容区它占据 700px 宽,横向排列很好看;可同一个组件被放进侧边栏,只有 280px 宽,横向排列立刻挤成一团,文字被压成竖条。

面对这个问题,传统的解法只有三种,而它们都不够好:

  • 解法一:给组件加一个修饰类。例如 .product-card--compact。问题在于,谁来决定用哪个类?只能是使用它的页面,于是组件的“自适应”变成了“被安排”。
  • 解法二:写一堆上下文选择器。例如 .sidebar .product-card { ... }。这会让组件与页面结构强耦合,一旦侧边栏换个位置,样式就失效。
  • 解法三:用媒体查询模拟。例如 @media (max-width: 768px) { ... }。可这依然在盯视口:桌面端侧边栏只有 280px,但视口有 1440px,媒体查询完全不会触发。

这三种解法的共同病根是:媒体查询无法回答“我这个组件现在有多宽”这个问题。而容器查询正是为了回答它而诞生的。

/* ❌ 传统做法:组件依赖外部上下文 */
.product-card { /* 默认样式 */ }
.sidebar .product-card {
  flex-direction: column;
}
/* 组件被绑死在 .sidebar 上,换个地方就废了 */

/* ❌ 媒体查询做法:只看视口,不看容器 */
@media (max-width: 768px) {
  .product-card { flex-direction: column; }
}
/* 桌面端侧边栏只有 280px,这里不会触发 */

/* ✅ 容器查询做法:组件自己说了算 */
.card-wrap {
  container-type: inline-size;
}
@container (min-width: 400px) {
  .product-card {
    flex-direction: row;
  }
}
媒体查询 感知视口宽度,与组件所处位置无关
上下文选择器 组件与页面结构强耦合,难以复用
容器查询 感知自身容器的宽度,组件真正自包含
关键结论 媒体查询管页面,容器查询管组件,两者互补而非替代

一句话记住区别

媒体查询问的是“屏幕有多大”,容器查询问的是“我住在多大的房子里”。一个组件被放进任何容器,都能自己决定该怎么排布——这才是真正意义上的组件化响应式。

核心语法:三行代码建立第一个容器

容器查询的语法出奇地简单,核心只有两步:先声明谁是容器,再写条件。

/* 第一步:在父元素上声明“我是一个查询容器” */
.card-wrap {
  container-type: inline-size;
  container-name: card; /* 可选,但强烈建议命名 */
}

/* 也可以写成简写 */
.card-wrap {
  container: card / inline-size;
}

/* 第二步:用 @container 写条件 */
@container card (min-width: 400px) {
  .product-card {
    flex-direction: row;
    gap: 16px;
  }
}

/* 不写名字也可以,就近匹配最近的容器 */
@container (min-width: 400px) {
  .product-card { flex-direction: row; }
}

这里有两个容易踩坑的细节:

  • 容器不能查询自己。@container 只对“容器的后代元素”生效。如果你给 .product-card 自己加了 container-type,再写 @container 去改它自己的样式,是不会生效的。
  • 命名不是可有可无的装饰。当页面里存在多个嵌套容器时,不命名会让浏览器采用“最近祖先”策略,结果往往出人意料。命名能让规则精确落位。

媒体查询与容器查询的语法对照

维度 媒体查询 容器查询
规则前缀 @media @container
作用对象 整个视口 / 设备 最近(或指定名字)的容器
需要声明 不需要 父元素需设置 container-type
可用单位 px / em / rem 等 同左,另加 cqw / cqi 等容器单位
典型场景 页面级栅格、导航折叠 组件级布局切换、卡片自适应

两者并不是非此即彼的关系。一个成熟的项目通常是这样分工的:页面骨架用媒体查询,组件内部用容器查询。前者决定“页面有几种宏观布局”,后者决定“组件在不同空间里怎么表现”。

container-type 三种取值:你在建立什么样的容器

container-type 决定了容器能响应哪种查询。它的选择不仅影响能不能查,还会影响元素的布局行为,这一点经常被忽略。

/* normal:默认值,不建立查询容器 */
.a { container-type: normal; }
/* 元素行为完全不变,但可以用于 style() 查询 */

/* inline-size:只查询水平方向(最常用) */
.b { container-type: inline-size; }
/* 等价于同时应用: */
/* contain: layout inline-size style; */
/* 元素的宽度由外部决定,不受内部内容影响 */

/* size:水平和垂直方向都可查询 */
.c {
  container-type: size;
  width: 100%;
  height: 300px; /* 必须有明确高度 */
}
/* 等价于 contain: size layout style paint; */
/* 尺寸完全独立,不依赖内容撑开 */

/* style:只查询自定义属性,不查询尺寸 */
.d { container-type: style; }
normal 零成本,但无法用于尺寸查询
inline-size 首选。适合绝大多数卡片、面板、列表项
size 需要固定高度,否则元素会塌陷为 0
style 用于主题切换、状态驱动的样式查询

inline-size 的副作用:宽度不再被内容撑开

这是最容易让人困惑的一点。给一个元素设置 container-type: inline-size 后,它会应用 contain: inline-size,这意味着元素的宽度不再受内容影响。

举个例子:一个 display: inline-block 的标签,原本宽度由文字长度决定。一旦加上 container-type: inline-size,它的宽度就会变成由外部约束(通常是撑满父容器或收缩为 0)。如果你只是想让一个行内元素变成容器,记得额外给它一个明确的宽度或 display: block。

/* ⚠️ 陷阱:inline-block 元素加容器类型后宽度行为改变 */
.badge {
  display: inline-block;
  container-type: inline-size;
  /* 宽度不再是文字宽度,可能塌成 0 */
}

/* ✅ 修正:明确宽度,或改用块级布局 */
.badge {
  display: inline-block;
  container-type: inline-size;
  width: fit-content; /* 显式指定 */
  min-width: 4em;
}

/* ✅ 更好的做法:容器放在外层,组件放在内层 */
.badge-wrap {
  container-type: inline-size;
  display: inline-block;
}
.badge { display: inline-block; }

size 类型的适用场景

container-type: size 需要容器在两个方向上都有明确尺寸,因此它适合那些尺寸已经由外部完全确定的场景,例如:

  • 固定高度的图表容器,内部图例要根据高度决定横排还是竖排;
  • 画布类组件(地图、可视化面板),需要同时感知宽高;
  • 全屏转场中的媒体容器,需要按宽高比决定裁切方式。

对于普通的响应式卡片,inline-size 几乎总是更安全、更合适的选择。因为它的约束更弱,不会让元素意外塌陷。

交互实验室:亲手拖一拖容器宽度

下面这个实验台把容器宽度暴露成一个滑块。拖动滑块,你会看到同一份 HTML 结构在窄容器里是纵向布局,宽过 340px 后切换成横向,再宽过 520px 后缩略图放大、标题字号提升。这就是容器查询的全部魔力——它不关心你的屏幕有多大,只关心自己住在多大的盒子里。

自适应卡片组件

容器宽度决定内部布局。窄的时候纵向堆叠,宽的时候横向并排。

container inline-size

同一份 HTML

没有任何修饰类,没有任何上下文选择器,结构完全保持一致。

@container demo
容器内容宽度: 340px 命中断点: column 生效规则: @container demo (min-width: 0)

观察要点:

  • 容器宽度在 0~339px 时,卡片是纵向布局,缩略图占满整行;
  • 跨过 340px,卡片变成横向,缩略图固定为 80×80;
  • 跨过 520px,缩略图放大到 112×112,标题与正文同步放大;
  • 整个过程没有任何 JS 参与布局计算——JS 只负责改变容器宽度,其余全部由 CSS 完成。
/* 这个实验台背后的全部 CSS */
.cq-stage {
  container-type: inline-size;
  container-name: demo;
}

.cq-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

@container demo (min-width: 340px) {
  .cq-card {
    flex-direction: row;
    align-items: center;
  }
  .cq-card-thumb {
    width: 80px;
    height: 80px;
  }
}

@container demo (min-width: 520px) {
  .cq-card-thumb {
    width: 112px;
    height: 112px;
  }
  .cq-card-body h4 { font-size: 1.2rem; }
}

容器查询单位:让尺寸随容器缩放

除了条件判断,容器查询还带来了一整套新的长度单位。它们以容器尺寸为基准,而不是视口尺寸——这意味着一个组件内部的字号、间距、圆角都可以随容器等比缩放。

单位 含义 典型用途
cqw 容器宽度的 1% 与容器宽度联动的尺寸
cqh 容器高度的 1% 需要容器高度的场景(size 类型)
cqi 容器内联轴尺寸的 1% 横向书写模式下等价于 cqw
cqb 容器块轴尺寸的 1% 纵向书写模式下等价于 cqh
cqmin cqi 与 cqb 中较小者 需要保证不溢出的场景
cqmax cqi 与 cqb 中较大者 需要保证足够醒目的场景
容器单位缩放
font-size: 7cqi · width: 60cqi
1cqi = 4.20px 标题 7cqi = 29.4px 进度条 60cqi = 252px

拖动滑块,你会看到标题字号、进度条宽度、说明文字同步缩放。这种效果如果用 vw 实现,就会跟着视口变化——而视口没变的时候,组件就不会有任何响应。容器单位让“缩放”第一次成为组件自己的行为。

/* 场景:一个随容器缩放的统计卡片 */
.stat-wrap {
  container-type: inline-size;
}

.stat-card {
  padding: 4cqi;
  border-radius: 2cqi;
}

.stat-value {
  /* 数字随容器放大,永远占据视觉重心 */
  font-size: clamp(1.5rem, 12cqi, 3.5rem);
  line-height: 1.1;
}

.stat-label {
  font-size: clamp(0.7rem, 3.5cqi, 1rem);
  letter-spacing: 0.04em;
}

/* ⚠️ 不要滥用:容器单位也会放大你的失误 */
/* 容器很窄时 12cqi 可能小于可读下限 */
/* 因此永远配合 clamp() 使用 */
cqi / cqw 横向缩放的首选,兼容性最好
cqh / cqb 需要容器有明确高度(size 类型)
cqmin / cqmax 处理极端宽高比时的安全网
注意事项 必须配合 clamp() 设置上下限,避免不可读

容器单位 vs 视口单位:一次直观对照

/* 视口单位:跟着浏览器窗口走 */
.hero-title { font-size: 5vw; }
/* 侧边栏里的同一组件,字号依然巨大,撑破布局 */

/* 容器单位:跟着组件所在的盒子走 */
.hero-title { font-size: 7cqi; }
/* 放进侧边栏自动变小,放进主区域自动变大 */

/* 现代写法:两者结合,兼顾全局节奏与局部适配 */
.hero-title {
  font-size: clamp(1.25rem, 4vw + 3cqi, 3rem);
}

双容器对比:同一组件,两种命运

下面这份对比图把完全相同的 HTML 结构分别放进 280px 和 600px 的容器里。请横向滚动查看:窄容器里它是纵向卡片,宽容器里它自动变成横向布局。整个过程没有任何额外类名、没有任何媒体查询。

容器 A · width: 280px
侧边栏卡片

宽度不足以横排,自动采用纵向布局,缩略图占满整行。

容器 B · width: 600px
主内容区卡片

空间充足,自动切换为横向布局,缩略图放大到 104×104,标题与正文字号同步提升,信息层级更清晰。

/* 两份 HTML 完全一致,差别只在外部容器宽度 */
<div class="dual-container" style="width:280px">
  <div class="mini-card"> ... </div>
</div>

<div class="dual-container" style="width:600px">
  <div class="mini-card"> ... </div>
</div>

/* CSS 只写一次 */
.dual-container {
  container-type: inline-size;
  container-name: duo;
}

@container duo (min-width: 360px) {
  .mini-card { flex-direction: row; }
  .mini-thumb { width: 76px; height: 76px; }
}

@container duo (min-width: 540px) {
  .mini-thumb { width: 104px; height: 104px; }
}

这个能力为什么重要

在组件库、设计系统、微前端的语境下,组件会被放进无数种容器里。如果组件只能靠外部传类名来适配,那它就不是真正可复用的组件,只是一个“半成品”。容器查询把适配逻辑收回到组件内部,让组件真正具备了自包含的响应能力。

实战一:打造一个自包含的卡片组件

让我们把前面的知识整合成一个完整的、可以直接放进项目的卡片组件。它需要满足:在任何容器里都能自适应、层级清晰、字号可读、动效自然。

/* ========== HTML 结构 ========== */
/* 外部容器负责建立查询上下文,组件本身不假设任何宽度 */
<div class="card-host">
  <article class="card">
    <div class="card__media">...</div>
    <div class="card__body">
      <h3 class="card__title">...</h3>
      <p class="card__desc">...</p>
      <div class="card__meta">...</div>
    </div>
  </article>
</div>

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

/* ========== 基础样式(移动优先) ========== */
.card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: #fff;
  border-radius: 12px;
  padding: clamp(12px, 4cqi, 20px);
  box-shadow: 0 1px 4px rgba(15,23,42,.08);
}

.card__media {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 8px;
  object-fit: cover;
  flex-shrink: 0;
}

.card__title {
  font-size: clamp(1rem, 5cqi, 1.35rem);
  line-height: 1.35;
}

.card__desc {
  font-size: clamp(0.82rem, 3.4cqi, 0.95rem);
  color: #64748b;
  line-height: 1.65;
}

/* ========== 断点一:够宽了,改横排 ========== */
@container card (min-width: 360px) {
  .card {
    flex-direction: row;
    align-items: center;
    gap: 16px;
  }
  .card__media {
    width: 96px;
    aspect-ratio: 1 / 1;
  }
}

/* ========== 断点二:很宽了,展示更多信息 ========== */
@container card (min-width: 560px) {
  .card { gap: 22px; }
  .card__media {
    width: 140px;
  }
  .card__meta {
    display: flex; /* 窄容器里隐藏的元信息,宽容器里显示 */
    gap: 8px;
  }
}

/* ========== 容器极窄时的兜底 ========== */
@container card (max-width: 220px) {
  .card__desc { display: none; }
  .card__media { aspect-ratio: 4 / 3; }
}

三个设计要点

  • 基础样式面向最窄场景。默认就是纵向、紧凑、字号偏小;容器变宽时才逐步“解锁”更多布局可能。这与移动优先的思路一致,只是判断依据从视口变成了容器。
  • 用 clamp() 兜住字号。容器单位负责缩放,clamp() 负责保证可读性。只写 5cqi 在极窄容器里会小到看不清,在极宽容器里又可能大得离谱。
  • 条件性显示信息。容器窄的时候隐藏次要描述,宽的时候再展示——这是容器查询相对于媒体查询的独特优势,因为同一个页面里不同位置的组件状态可以完全不同。
不要这样
给卡片自己加 container-type,然后写 @container 改卡片自己的布局。容器不能查询自身,这条规则永远不会生效。
应该这样
在卡片外层套一个宿主元素(.card-host)承担容器角色,卡片本身保持纯粹的组件样式。

实战二:自适应侧边栏与主内容布局

页面级的双栏布局也可以从容器查询中获益。设想一个仪表盘:左侧是筛选面板,右侧是数据区。当筛选面板本身被折叠得很窄时,它内部的表单应该自动变成图标模式或纵向堆叠——而这取决于它自己的宽度,不是视口。

/* ========== 布局骨架 ========== */
.dashboard {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
}

/* 侧边栏本身成为查询容器 */
.filters {
  container-type: inline-size;
  container-name: filters;
  width: 260px;
  transition: width .3s ease;
}
.filters.collapsed { width: 72px; }

/* ========== 极窄:只显示图标 ========== */
@container filters (max-width: 100px) {
  .filter-group__label {
    /* 视觉隐藏但保留给屏幕阅读器 */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }
  .filter-group {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
}

/* ========== 中等宽度:标签在上,控件在下 ========== */
@container filters (min-width: 101px) and (max-width: 220px) {
  .filter-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .filter-group__label {
    font-size: 0.78rem;
  }
}

/* ========== 宽敞:标签与控件同一行 ========== */
@container filters (min-width: 221px) {
  .filter-group {
    display: grid;
    grid-template-columns: 80px 1fr;
    align-items: center;
    gap: 10px;
  }
}
/* ========== 主内容区同样可以是容器 ========== */
.main-panel {
  container-type: inline-size;
  container-name: main;
}

/* 数据网格:按主区域宽度决定列数 */
.metric-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

@container main (min-width: 420px) {
  .metric-grid { grid-template-columns: repeat(2, 1fr); }
}

@container main (min-width: 720px) {
  .metric-grid { grid-template-columns: repeat(4, 1fr); }
}

@container main (min-width: 1080px) {
  .metric-grid { grid-template-columns: repeat(6, 1fr); }
}

/* 传统做法:媒体查询只能按视口猜 */
@media (min-width: 1200px) {
  .metric-grid { grid-template-columns: repeat(4, 1fr); }
}
/* 侧边栏展开/折叠时,媒体查询完全无感 */
媒体查询痛点 侧边栏折叠后主区域变宽,视口却没变
容器查询解法 主区域自己变宽,网格列数立即响应
组合使用 视口决定侧边栏是否折叠,容器决定内部布局
关键收益 同一套代码在展开/折叠两种状态下都成立

为什么这在仪表盘里特别有价值

仪表盘的特点是布局状态多变:用户可以折叠侧边栏、可以拖拽分栏、可以全屏某个面板。这些变化都不改变视口宽度,但都会改变组件的可用空间。用媒体查询处理这些场景,你几乎必然要写一堆状态类;用容器查询,组件自己就能感知空间变化,代码量反而更少。

实战三:从数据表到卡片列表的无缝切换

数据表格是响应式设计里最头疼的组件之一。传统做法是:视口小于 768px 时把表格转成卡片。但如果这个表格被放在一个窄的面板里呢?视口很宽,表格依然会被挤爆。

容器查询让表格可以真正按“自己有多宽”来决定形态。

/* ========== HTML 结构 ========== */
<div class="table-host">
  <table class="data-table">
    <thead><tr><th>名称</th><th>状态</th><th>金额</th></tr></thead>
    <tbody>
      <tr>
        <td data-label="名称">订单 A</td>
        <td data-label="状态">已支付</td>
        <td data-label="金额">¥ 1,280</td>
      </tr>
    </tbody>
  </table>
</div>

/* ========== 容器声明 ========== */
.table-host {
  container-type: inline-size;
  container-name: table;
}

/* ========== 表格态(宽容器) ========== */
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

/* ========== 卡片态(窄容器) ========== */
@container table (max-width: 520px) {
  .data-table thead {
    /* 表头视觉隐藏,但保留给屏幕阅读器 */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .data-table tbody tr {
    display: block;
    background: #fff;
    border-radius: 10px;
    padding: 12px;
    margin-bottom: 10px;
    box-shadow: 0 1px 4px rgba(15,23,42,.08);
  }

  .data-table td {
    display: flex;
    justify-content: space-between;
    padding: 6px 0;
    border: none;
  }

  .data-table td::before {
    content: attr(data-label);
    font-weight: 600;
    color: #64748b;
    font-size: 0.78rem;
  }
}

/* ========== 极窄:进一步精简 ========== */
@container table (max-width: 300px) {
  .data-table td {
    flex-direction: column;
    gap: 2px;
  }
}

这个方案的三个优势

  • 一份 HTML,两种形态。不需要为移动端单独渲染一套 DOM,也就不存在“两套结构不同步”的维护噩梦。
  • 无障碍友好。表头用视觉隐藏而非 display: none,屏幕阅读器依然能正确读出表格语义;data-label 让卡片态的每个单元格都有明确的字段名。
  • 位置无关。表格放进主内容区就显示为表,放进窄侧栏就变成卡片列表——不需要任何外部干预。

命名容器与嵌套:当页面里有多个容器

真实项目里,容器往往不止一层。一个卡片放在列表里,列表放在侧边栏里,侧边栏又放在页面栅格里——每一层都可能成为容器。这时候命名就成了精确控制的关键。

/* ========== 多层容器命名 ========== */
.page-grid {
  container-type: inline-size;
  container-name: page;
}

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

.card-host {
  container-type: inline-size;
  container-name: card;
}

/* ========== 精确指定查询哪一层 ========== */
/* 只看最近的 card 容器 */
@container card (min-width: 400px) {
  .card { flex-direction: row; }
}

/* 只看 sidebar 容器(跳过中间的 card) */
@container sidebar (min-width: 600px) {
  .card { box-shadow: 0 4px 12px rgba(15,23,42,.12); }
}

/* ========== 一个元素可以同时有多个名字 ========== */
.card-host {
  container-name: card card-host-host;
}

/* ========== 也可以用简写一次性声明 ========== */
.card-host {
  container: card / inline-size;
}

/* ========== 没有名字时:就近匹配最近的可查询容器 ========== */
@container (min-width: 400px) {
  /* 谁最近就查谁,行为可能随 DOM 变化而改变 */
  /* 因此生产环境建议始终命名 */
  .card { flex-direction: row; }
}
命名查询 精确、稳定、可预测
匿名查询 就近匹配,DOM 变动时可能失效
多个名字 一个元素可被多种语义引用
注意事项 容器不能查询自身,必须作用于后代

容器名称的命名规范建议

  • 按语义命名,不按尺寸:用 card、sidebar、main,而不是 narrow、wide。因为容器的宽度是变化的,语义是不变的。
  • 避免与元素类名冲突:容器名有独立的命名空间,与类名互不干扰,但为了避免阅读混乱,建议加一个统一前缀,如 cq-card。
  • 在组件根节点声明容器:让组件的“容器身份”一目了然,而不是藏在某个中间层。
  • 不要给所有元素都加容器类型:每个 container-type 都会引入 containment 约束,可能改变布局行为,也会带来额外开销。
/* ========== 一个典型的组件级命名方案 ========== */
/* 统一前缀,语义清晰,避免歧义 */

.cq-card-host {
  container: cq-card / inline-size;
}

.cq-sidebar-host {
  container: cq-sidebar / inline-size;
}

.cq-main-host {
  container: cq-main / inline-size;
}

/* 使用时语义明确,一眼可知查询的是哪一层 */
@container cq-card (min-width: 400px) { ... }
@container cq-sidebar (min-width: 280px) { ... }

style() 查询:不止尺寸,还能查询样式状态

容器查询的能力不止于尺寸。通过 @container style(),你可以查询容器上的自定义属性值,从而实现主题切换、状态驱动的样式变化。这是容器查询里最被低估的能力。

样式查询演示组件
点击下方按钮切换容器上的 --theme 自定义属性,观察组件外观如何随之改变。所有逻辑都由 CSS 完成,JS 只负责改变一个变量的值。
当前 --theme: light 命中规则: @container style(--theme: light)
/* ========== 声明一个样式容器 ========== */
.style-widget {
  container-type: style; /* 注意:不是 inline-size */
  --theme: light;
}

/* ========== 按自定义属性值匹配 ========== */
@container style(--theme: dark) {
  .style-widget {
    background: #0f172a;
    border-color: #1e293b;
  }
  .style-widget .sw-title { color: #f8fafc; }
  .style-widget .sw-text { color: #94a3b8; }
}

@container style(--theme: brand) {
  .style-widget {
    background: linear-gradient(135deg, #2563eb, #1d4ed8);
    color: white;
  }
}

/* ========== 也可以做布尔式判断 ========== */
/* 只要 --dense 有值(非 initial)就命中 */
@container style(--dense) {
  .list-row { padding: 4px 8px; }
}

/* ========== 结合尺寸查询一起使用 ========== */
.panel {
  container: panel / inline-size style;
  --mode: compact;
}

@container panel (min-width: 600px) and style(--mode: compact) {
  /* 同时满足尺寸与样式条件 */
  .panel-body { columns: 2; }
}

style() 查询的典型用途

  • 主题注入:外层容器设置 --theme: dark,内部所有组件自动切换暗色样式,无需给每个组件传参。
  • 密度控制:表格容器设置 --dense,内部的单元格、按钮自动收紧内边距。
  • 状态驱动:容器设置 --state: loading,内部自动显示骨架屏、禁用交互。
  • 布局变体:同一个组件在 --layout: compact 的容器里显示精简版,在默认容器里显示完整版。

需要注意:style() 查询目前主要用于自定义属性,不能查询任意 CSS 属性值(例如查询 display 或 color 是不行的)。它的设计意图就是让开发者通过自定义属性传递语义状态,因此使用时也要遵循这个约定。

scroll-state() 查询:感知滚动与吸附状态

容器查询家族还在持续扩展。@container scroll-state() 让元素可以感知自己是否被吸附(stuck)、是否可滚动、是否滚动到了边缘——这些过去只能靠 JS 监听 scroll 事件才能实现的能力,现在直接写进 CSS。

/* ========== 感知吸附状态 ========== */
/* 让一个 sticky 表头在被吸附时改变样式 */
.scroll-area {
  container-type: scroll-state;
  overflow-y: auto;
  height: 400px;
}

.sticky-header {
  position: sticky;
  top: 0;
  background: #fff;
  transition: box-shadow .2s ease;
}

@container scroll-state(stuck: top) {
  .sticky-header {
    box-shadow: 0 4px 12px rgba(15,23,42,.12);
    border-bottom: 1px solid #e2e8f0;
  }
}

/* ========== 感知滚动方向与位置 ========== */
/* 滚动到顶部时隐藏“回到顶部”按钮 */
@container scroll-state(scrollable: top) {
  .back-to-top { opacity: 1; pointer-events: auto; }
}

@container scroll-state(scrollable: none) {
  .back-to-top { opacity: 0; pointer-events: none; }
}

/* ========== 感知是否溢出 ========== */
@container scroll-state(overflowing: bottom) {
  .fade-mask { opacity: 1; }
}

/* ========== 传统 JS 方案的对比 ========== */
/* 需要在 scroll 事件里读取 scrollTop、比较阈值 */
/* 再手动加/删 class —— 容易掉帧,也容易写漏 */
area.addEventListener('scroll', () => {
  const stuck = area.scrollTop > header.offsetTop;
  header.classList.toggle('is-stuck', stuck);
}, { passive: true });

能力边界与使用建议

  • 目前支持度较低:scroll-state() 在 Chrome 133+ 才逐步可用,生产环境务必配合 @supports 与 JS 退化方案。
  • 适合纯视觉反馈:吸附阴影、渐隐遮罩、回到顶部按钮显隐——这些“有更好,没有也能用”的效果最适合用它。
  • 不适合承载关键逻辑:不要用它来控制业务数据的加载或权限判断,那不是 CSS 的职责。

与 Grid、Flex 的配合:让布局引擎先干活

容器查询不是万能的,它擅长的是“在给定空间里做取舍”。而很多自适应问题,其实布局引擎自己就能解决——用容器查询之前,先确认是不是在重复造轮子。

/* ========== 场景一:自动换行的卡片流 ========== */
/* ✅ 用 auto-fit + minmax,一行代码搞定 */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px;
}
/* 不需要任何查询,浏览器自动决定每行几个 */

/* ========== 场景二:侧边栏宽度由内容决定 ========== */
/* ✅ 用 Grid 的 auto 轨道 */
.layout {
  display: grid;
  grid-template-columns: minmax(200px, 280px) 1fr;
  gap: 20px;
}

/* ========== 场景三:真正需要容器查询的时刻 ========== */
/* 当布局已定,但组件内部需要“重新排版”时 */
.card-host {
  container: card / inline-size;
}

/* 这才是容器查询的主场:内部结构切换 */
@container card (min-width: 420px) {
  .card { flex-direction: row; }
  .card__media { width: 120px; aspect-ratio: 1; }
  .card__meta { display: flex; }
}
Grid auto-fit 自动计算列数,无需任何查询
Flex wrap 自动换行,适合标签、工具栏
clamp() 字号与间距的连续缩放
容器查询 结构切换:横排 ↔ 竖排、显示 ↔ 隐藏

决策流程:什么时候该用容器查询

1
能自动吗?
auto-fit / wrap
如果布局引擎本身就能处理(网格自动列数、弹性换行),那就不要写查询。代码更少,性能更好。
2
能连续吗?
clamp / cqi
如果只是尺寸的连续缩放(字号、间距、圆角),用 clamp() 配合容器单位即可,无需断点。
3
要换结构吗?
@container
如果布局需要在某个宽度发生“质变”——横排变竖排、表格变卡片、显示变隐藏,这才是容器查询的用武之地。
4
断点在哪?
按内容定,不按设备
容器查询的断点应该由组件内容决定,而不是套用 768/1024 这类设备断点。把容器慢慢收窄,看它在哪个宽度开始变得难看——那就是断点。

一个实用的经验:先让布局引擎做它能做的事,再用容器查询处理它做不到的事。很多开发者一上来就写一堆 @container,结果发现代码量大、断点难维护,反而不如 repeat(auto-fit, minmax()) 来得干净。

性能考量与常见陷阱

容器查询本身不会让页面变慢——它读取的是浏览器已有的布局信息,判断成本极低。但使用不当仍然会踩坑,尤其是对 containment 约束理解不足的时候。

/* ❌ 陷阱一:容器太多,containment 到处生效 */
.everything {
  container-type: inline-size;
}
/* 每个元素都变成容器,宽度不再被内容撑开,布局崩塌 */

/* ❌ 陷阱二:size 类型但没有固定高度 */
.panel {
  container-type: size;
  /* 没有 height,容器高度为 0,内部全部塌陷 */
}

/* ❌ 陷阱三:容器查询自身 */
.box {
  container-type: inline-size;
}
@container (min-width: 400px) {
  .box { padding: 20px; } /* 不会生效 */
}

/* ❌ 陷阱四:断点套用设备尺寸 */
@container (min-width: 768px) { ... }
/* 768 是视口断点,容器宽度通常远小于它 */
/* 结果:规则永远不触发,或触发得莫名其妙 */

/* ✅ 正解:按组件自身的内容临界点定断点 */
/* 把卡片从 500px 慢慢收窄,观察: */
/* 大约 360px 时横排开始拥挤 → 断点定在 360px */
@container card (min-width: 360px) { ... }
containment 副作用 宽度不再由内容决定,可能塌陷
size 类型 必须显式设置宽高,否则高度为 0
断点选择 按内容临界点,不按设备断点
性能 判断成本极低,但避免容器数量爆炸

关于性能的三个事实

  • 容器查询的判断本身几乎不花钱。浏览器在布局阶段就已经知道每个容器的尺寸,查询只是读取并比较,不产生额外的重排。
  • 代价在于 containment 约束。当元素声明了 container-type,它内部的变化不会再影响外部布局,这通常减少计算量;但如果因此需要额外的显式尺寸,可能引入新的布局约束。
  • 真正需要警惕的是过渡动画。容器宽度变化时,如果内部有大量基于容器查询的样式切换,并且都带 transition,可能会同时触发多组布局计算。建议只给必要属性加过渡,并且时长控制在 300ms 以内。

调试容器查询的实用技巧

/* ========== 技巧一:给容器加临时可视化 ========== */
.card-host {
  container: card / inline-size;
}
/* 调试期临时打开,用 outline 标出容器边界 */
.debug-containers .card-host {
  outline: 2px dashed #ef4444;
  outline-offset: -2px;
}

/* ========== 技巧二:用伪元素显示容器当前尺寸 ========== */
.debug-containers .card-host::after {
  content: 'card container';
  position: absolute;
  top: 0;
  left: 0;
  background: #ef4444;
  color: white;
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 0 0 4px 0;
}

/* ========== 技巧三:DevTools 中的容器标记 ========== */
/* Elements 面板中,容器元素会显示 container 徽章 */
/* Styles 面板中,@container 规则会标注命中状态 */
/* 未命中的规则会灰显,方便快速定位 */

兼容性与渐进增强策略

容器查询的浏览器支持已经相当不错。尺寸查询(inline-size)在主流浏览器中都可用,而较新的 style()、scroll-state() 则还需要时间。

container-type: inline-size
96%+
Chrome 105 / Safari 16
@container 尺寸查询
96%+
Chrome 105 / Safari 16
cqw / cqi 单位
96%+
Chrome 105 / Safari 16
container-name
96%+
Chrome 105 / Safari 16
@container style()
70%+
Chrome 111 / Safari 18
@container scroll-state()
35%+
Chrome 133+
/* ========== 渐进增强:基础样式先保证可用 ========== */
/* 默认:窄容器布局,所有浏览器都能正确显示 */
.card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 用 @supports 检测容器查询能力 */
@supports (container-type: inline-size) {
  .card-host {
    container: card / inline-size;
  }
  @container card (min-width: 400px) {
    .card { flex-direction: row; }
  }
}

/* ========== JS 侧的特性检测 ========== */
const hasCQ = CSS.supports('container-type', 'inline-size');
const hasStyleCQ = CSS.supports('container-type', 'style');

if (!hasCQ) {
  /* 退化:用 ResizeObserver 手动计算并加类 */
  const ro = new ResizeObserver((entries) => {
    entries.forEach((e) => {
      e.target.classList.toggle('is-wide', e.contentRect.width >= 400);
    });
  });
  document.querySelectorAll('.card-host').forEach((el) => ro.observe(el));
}

/* 对应的退化 CSS */
.card-host.is-wide .card {
  flex-direction: row;
}
尺寸查询 可直接用,无需退化方案
容器单位 直接可用,但建议配合 clamp()
style() 查询 用 @supports 包裹,或提供默认主题
scroll-state() 务必保留 JS 退化路径

退化策略的选择

  • 纯视觉增强:例如吸附阴影、渐隐遮罩。不支持时效果消失,但功能完全正常——最省事的退化方式。
  • 布局切换:例如卡片横排。退化时保持纵向布局,虽然不够好看,但依然可读可用。
  • 关键信息隐藏:如果某个信息只在宽容器里显示,请确保它在窄容器里不是必需的,否则退化后用户会丢失信息。
  • 需要精确控制时:用 ResizeObserver 做 JS 退化,代价是要维护两套逻辑,仅在必要时使用。

容器查询最佳实践清单

把全文的要点浓缩成一份可以直接贴在工位上的清单。

  • 媒体查询管页面,容器查询管组件:两者分工明确,不要用其中一个替代另一个。
  • 容器类型优先选 inline-size:只有在确实需要感知高度时才用 size。
  • 始终给容器命名:匿名查询依赖 DOM 就近匹配,脆弱且难以维护。
  • 容器不能查询自己:把 container-type 放在外层宿主元素上,组件本身保持干净。
  • 断点由内容决定:慢慢收窄容器,找到它开始“难看”的那个宽度,那就是断点。
  • 优先用布局引擎的能力:auto-fit、minmax()、flex-wrap 能解决的,别写查询。
  • 用 clamp() 限制容器单位:clamp(0.8rem, 3cqi, 1.2rem) 比裸写 3cqi 安全得多。
  • 注意 inline-size 的宽度约束:元素宽度不再被内容撑开,必要时显式指定尺寸。
  • 不要给所有元素加容器类型:容器数量爆炸会带来不必要的 containment 开销。
  • 过渡动画只加在必要属性上:容器宽度变化时,避免同时触发大量布局计算。
  • 用 @supports 包裹新特性:style() 与 scroll-state() 需要退化路径。
  • 调试时给容器加可视化边框:配合 DevTools 的 container 徽章,快速定位命中情况。
  • 组件库优先采用容器查询:这是让组件真正自包含、可复用的关键一步。
  • 在真实内容下测试:占位文字和真实文案的换行行为差异很大,断点必须用真实内容验证。
/* ========== 一套可直接复用的容器查询基线 ========== */

/* 1. 组件宿主:统一声明容器 */
.cq-host {
  container-type: inline-size;
  container-name: cq;
}

/* 2. 组件基础样式:面向最窄场景 */
.cq-item {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: clamp(12px, 4cqi, 20px);
}

/* 3. 内容尺寸连续缩放 */
.cq-item__title {
  font-size: clamp(1rem, 5cqi, 1.35rem);
}
.cq-item__desc {
  font-size: clamp(0.82rem, 3.4cqi, 0.95rem);
}

/* 4. 结构切换断点 */
@container cq (min-width: 360px) {
  .cq-item {
    flex-direction: row;
    align-items: center;
  }
}

/* 5. 更宽时的信息增强 */
@container cq (min-width: 560px) {
  .cq-item__meta { display: flex; }
}

/* 6. 极窄时精简内容 */
@container cq (max-width: 220px) {
  .cq-item__desc { display: none; }
}

/* 7. 渐进增强包裹 */
@supports (container-type: inline-size) {
  /* 容器查询规则集中放在这里 */
}

容器查询真正改变的,不是某一行 CSS 的写法,而是我们思考响应式的起点。过去我们习惯从“屏幕有多大”出发,把页面切成几档;现在我们可以从“组件住在哪里”出发,让每个组件自己决定该如何呈现。当组件库里的每一个成员都具备这种能力时,页面级的响应式设计就会变得前所未有的轻松——因为你不再需要为每一种使用场景准备一个变体。

建议你在下一个项目里,挑一个最常被复用的组件(卡片、列表项、表单项都行),把它改成容器查询驱动。改完之后,试着把它放进最窄和最宽的两个位置,看看它是否都能自然成立。那一刻你大概会意识到:原来响应式设计可以这么省心。