先说一个去年让我头疼了整整两天的场景。
我们给某个工厂做了一个能耗监控大屏。大屏上有 5 个同类型的「能耗趋势卡片」------左侧侧边栏放了 2 个,中间主区域 3 个。设计稿上,侧边栏的卡片是「图表在上、数据在下」的竖排紧凑布局,而主区域的卡片是「数据在左、图表在右」的横排舒展布局。
一开始,我写了两个组件:EnergyCardCompact 和 EnergyCardWide。后来需求加了一个右侧弹出面板------里面也需要一个同样的卡片,但布局又不一样。再后来客户说移动端也要能看......你猜怎么着,我写了四个组件。
这不是组件的错。这是 Media Queries 的局限性在坑我们。
传统响应式的核心是 @media,它问的问题是:"浏览器窗口多宽?" 然后给所有组件发号施令:
scss
/* 传统做法:视口决定一切 */
@media (min-width: 1920px) {
.chart-card { /* 大屏布局 */ }
}
@media (max-width: 768px) {
.chart-card { /* 移动端布局 */ }
}
这在页面级布局上挺好用。但只要你的组件会出现在不同宽度的父容器里,Media Queries 立刻就漏了。
比如我们组态大屏常见的场景:
css
┌─────────────────────────────────────────────┐
│ Header (1920px 全宽) │
├──────────┬──────────────────────────────────┤
│Sidebar │ Main Content │
│ 320px │ 1400px │
│ │ │
│ [Card1] │ [Card2] [Card3] [Card4] │
│ [窄] │ [宽] [宽] [宽] │
└──────────┴──────────────────────────────────┘
同一个 Card 组件在 320px 宽的侧边栏和 460px 宽的主区域要呈现出完全不同的布局,但 @media 只看视口的 1920px------它根本不知道你的卡片被塞进了哪个犄角旮旯。
这就是 Container Queries(容器查询) 要解决的问题。
Container Queries 是什么?
一句话:让 CSS 基于父容器的尺寸来决定样式,而不是基于浏览器视口。
2023 年初,主流浏览器全面支持 Container Queries;到 2026 年,所有主流浏览器(Chrome、Edge、Firefox、Safari)都已稳定支持,包括移动端。Can I Use 上显示全球覆盖率已超过 96%。
核心 API 就两段:
css
/* 1. 声明一个容器 */
.card-wrapper {
container-type: inline-size;
container-name: card-container;
}
/* 2. 按容器尺寸查询 */
@container card-container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@container card-container (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}
注意到关键了吗?这里的 min-width: 400px 不是指视口宽度,而是 .card-wrapper 这个容器的宽度 。把同一个 .card 组件放进 320px 的侧边栏和 460px 的主区域,它会自动切换布局。不需要 JavaScript,不需要 ResizeObserver,不需要多个变体组件。
实战:重构大屏指标卡片
回到我们的大屏场景,看看用 Container Queries 怎么重构那个「能耗趋势卡片」。
组件结构
xml
<div class="card-wrapper">
<div class="metric-card">
<div class="card-header">
<span class="card-title">今日能耗</span>
<span class="card-badge">实时</span>
</div>
<div class="card-body">
<div class="chart-area">
<canvas id="chart-electricity"></canvas>
</div>
<div class="data-area">
<div class="metric-value">12,847 <small>kWh</small></div>
<div class="metric-trend up">↑ 3.2%</div>
<div class="metric-detail">
<span>峰值:1,240 kW</span>
<span>谷值:280 kW</span>
</div>
</div>
</div>
</div>
</div>
CSS:三档自适应
css
/* 声明容器------关键一步 */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* === 窄容器:≤ 349px === */
/* 侧边栏、弹出面板场景 */
@container card (max-width: 349px) {
.card-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.chart-area {
height: 120px;
/* 小图 + 数据纵向堆叠 */
}
.data-area {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.metric-value {
font-size: 1.4rem;
}
.metric-detail {
display: none; /* 太窄了,隐藏细节 */
}
.card-badge {
display: none; /* badge 也省掉 */
}
}
/* === 中等容器:350px ~ 549px === */
/* 主区域标准卡片 */
@container card (min-width: 350px) and (max-width: 549px) {
.card-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-items: center;
}
.chart-area {
height: 160px;
/* 图表和数据并排,各占一半 */
}
.data-area {
display: flex;
flex-direction: column;
gap: 8px;
padding-left: 8px;
}
.metric-value {
font-size: 1.8rem;
}
.metric-detail {
display: flex;
flex-direction: column;
font-size: 0.8rem;
color: #8899aa;
}
.card-badge {
display: inline-block;
padding: 2px 6px;
background: #00d68f22;
color: #00d68f;
border-radius: 4px;
font-size: 0.7rem;
}
}
/* === 宽容器:≥ 550px === */
/* 全宽展示、仪表板焦点场景 */
@container card (min-width: 550px) {
.card-body {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 16px;
}
.chart-area {
height: 200px;
/* 图表区域更大 */
}
.data-area {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
}
.metric-value {
font-size: 2.2rem;
}
.metric-detail {
display: flex;
gap: 16px;
flex-wrap: wrap;
font-size: 0.85rem;
}
}
效果对比
同一份 HTML,同一个组件,放进不同宽度的容器后:
| 容器 | 宽度 | 布局 | 图表大小 | 数据位置 | 细节展示 |
|---|---|---|---|---|---|
| 侧边栏 | 320px | 纵向堆叠 | 120px | 图下方 | 无 |
| 主区域 | 460px | 左右分栏 | 160px | 图右侧 | 有 |
| 全宽区 | 700px | 2:1 分栏 | 200px | 图右侧 | 有+横排 |
不需要写四个组件。不需要 ResizeObserver 监听宽度再手动切 CSS 类。不需要 useEffect 里搞一堆条件判断。CSS 层就搞定了。
和大屏 scale 适配方案怎么配合?
做过大屏的前端都知道,可视化大屏有一种主流的适配方式:CSS Transform Scale。简单说就是设计稿按 1920×1080 做,然后等比缩放到实际屏幕。
javascript
// 经典的大屏 scale 适配
const scale = Math.min(
window.innerWidth / 1920,
window.innerHeight / 1080
);
document.querySelector('.screen-root').style.transform = `scale(${scale})`;
这种方案有个问题:scale 之后,你的组件在 CSS 眼里的尺寸还是 1920×1080 下的尺寸。你用 Container Queries 查的是缩放前的逻辑尺寸,而不是屏幕的物理尺寸。
正确的组合方式
以我们的实践经验:外层用 scale 做画布级别的等比缩放,内层组件用 Container Queries 做弹性布局。
scss
Layer 1 (画布层): scale 适配 → 保证设计稿还原度
Layer 2 (组件层): Container Queries → 保证组件的局部弹性
Layer 3 (内容层): clamp() / min() / max() → 保证字体和间距的自然缩放
举个例子:
css
/* 画布层:锁定 1920×1080,等比例 scale */
.screen-root {
width: 1920px;
height: 1080px;
transform-origin: 0 0;
/* scale 由 JS 动态计算 */
}
/* 组件层:基于实际 layout 区域自适应 */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
/* 每个 grid cell 就是自然的 container */
.grid-cell {
container-type: inline-size;
container-name: dashboard-cell;
}
/* 组件内部用 clamp() 做微调 */
@container dashboard-cell (max-width: 400px) {
.value-text {
font-size: clamp(0.9rem, 3cqi, 1.5rem);
/* cqi = container query inline unit,基于容器宽度的百分比 */
}
}
这样,外层 scale 保证了你的设计稿在大屏、投影、拼接屏上都长一样;内层 Container Queries 保证了每个网格区域里的组件不需要被暴力的 transform 拖累,该自适应就自适应。
Container Query 单位:cqi、cqw、cqb、cqh
Container Queries 还带来了四个专用的 CSS 单位,比 vw/vh 灵活得多:
| 单位 | 含义 | 类比 |
|---|---|---|
cqw |
容器宽度的 1% | 容器版的 vw |
cqh |
容器高度的 1% | 容器版的 vh |
cqi |
容器 inline 轴的 1% | 通常=宽度 |
cqb |
容器 block 轴的 1% | 通常=高度 |
cqmin |
容器 min(宽, 高) 的 1% | 容器版的 vmin |
cqmax |
容器 max(宽, 高) 的 1% | 容器版的 vmax |
在大屏场景里 cqi 特别好用------你可以让图表的高度根据容器宽度自动调节:
less
@container card (min-width: 300px) {
.chart-area {
height: 40cqi; /* 容器宽度的 40% */
}
.metric-value {
font-size: clamp(1rem, 6cqi, 2.5rem);
/* 随容器宽度缩放,但有上下限 */
}
}
性能:Container Queries 会拖慢渲染吗?
一个常见的担心:每个容器都是一个潜在的布局触发点,会不会导致性能问题?
实际测试下来(Chrome 130+,Canvas 图表的 ECharts 实例在卡片内),结论是:
- 声明
container-type: inline-size本身开销极小,浏览器仅在此容器尺寸变化时才会重新计算内部查询 - 比
ResizeObserver+ JS 动态改 CSS 类的方式快得多------Container Queries 是在浏览器布局阶段处理的,不触发额外的 JS 重排 - 但不要每个 div 都声明为容器。只在真正需要自适应的地方声明。我们的经验是:只给 grid/flex 的单元格级别声明 container,不要全局撒胡椒面
一个简单的原则:如果这个元素会因为页面布局变化而产生不同宽度,并且你希望它的子元素对此做出响应------那就把它声明为 container。
Style Queries:容器查询的"兄弟"
2026 年,Container Style Queries 也进入了 Baseline(主流浏览器全面支持)。它允许你基于 CSS 自定义属性的值来做查询:
less
@container style(--theme: dark) {
.chart-background {
background: #1a1a2e;
}
.chart-axis {
color: #8899aa;
}
}
这对大屏的深色/浅色主题切换、告警级别样式切换非常有用。但注意 Style Queries 目前只支持查询 CSS 自定义属性(--xxx),不能查询任意 CSS 属性。
实际踩过的坑
经过了几个大屏项目的实际使用,总结三个最疼的坑:
1. 容器必须要有"containment"
container-type 会隐式创建一个 containment context。这意味着:容器内部的绝对定位元素会相对于这个容器定位,而不是更外层的祖先。
arduino
.card-wrapper {
container-type: inline-size;
/* 隐式创建了 containment */
/* position: relative 还是得手动加! */
}
如果你原来依赖某个绝对定位元素跑到容器外面(比如 tooltip、下拉菜单),Container Queries 会让你吃一惊。解决方案是用 CSS Anchor Positioning 或者把 tooltip 的 position 提升到外层。
2. 容器的高度不会自动被撑开
这个很反直觉:如果你只声明了 container-type: inline-size,那容器的高度仍然由内容决定。但你不能 在同一容器的 Container Query 里基于高度做查询。如果需要双向查询,声明 container-type: size(注意这可能导致高度塌陷,因为 size containment 会阻止内容撑开容器高度)。
实际项目里,inline-size 覆盖了 90% 的场景。高度查询的需求非常罕见。
3. 嵌套容器查询的命名问题
当组件嵌套时,用 container-name 给每个容器命名,避免查询串到错误的祖先容器:
less
/* 不好:未命名,可能匹配到错误的祖先 */
@container (min-width: 400px) { }
/* 好:明确指定查询哪个容器 */
@container dashboard-cell (min-width: 400px) { }
总结
Container Queries 在 2026 年已经不是"新特性"了,但我在团队里观察到,真正在生产项目中把它用起来的人还是少数。尤其是在组态大屏这个细分领域,很多人还停留在"一套 scale 走天下"或者"写 N 个变体组件"的阶段。
实际上,Container Queries 和 scale 适配不冲突,它们是互补的:
- scale 负责"画布级"还原------保证大屏在不同物理分辨率上看起来一致
- Container Queries 负责"组件级"自适应------让同一个组件在不同布局位置智能切换形态
如果你正在做一个新的组态大屏项目,强烈建议把 Container Queries 放进你的 CSS 工具箱。它不需要额外的运行时开销、不需要多写 JS、不需要拆分组件变体------就是纯 CSS,而纯 CSS 的东西,永远是前端最可靠的。